Merge branch 'develop' into feature/legacy-download
This commit is contained in:
commit
6833c68e40
|
|
@ -0,0 +1,21 @@
|
|||
ActiveAdmin.register JamRuby::AppFeature, as: 'App Features' do
|
||||
|
||||
menu parent: 'Misc', label: 'App Features'
|
||||
|
||||
config.sort_order = 'created_at ASC'
|
||||
config.batch_actions = false
|
||||
config.filters = false
|
||||
config.per_page = 50
|
||||
config.paginate = true
|
||||
|
||||
form do |f|
|
||||
f.inputs 'Fields' do
|
||||
f.input(:feature_type, as: :select, collection: JamRuby::AppFeature::FEATURE_TYPES)
|
||||
f.input(:handle, :input_html => { :maxlength => 1025 })
|
||||
f.input(:is_enabled, as: :boolean)
|
||||
f.input(:env, as: :select, collection: %w(production staging development))
|
||||
end
|
||||
f.actions
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -4,4 +4,5 @@ REACT_APP_ORIGIN=jamkazam.local
|
|||
REACT_APP_CLIENT_BASE_URL=http://www.jamkazam.local:3000
|
||||
REACT_APP_API_BASE_URL=http://www.jamkazam.local:3000/api
|
||||
REACT_APP_BITBUCKET_BUILD_NUMBER=dev
|
||||
REACT_APP_BITBUCKET_COMMIT=dev
|
||||
REACT_APP_BITBUCKET_COMMIT=dev
|
||||
REACT_APP_ENV=development
|
||||
|
|
@ -4,4 +4,5 @@ REACT_APP_ORIGIN=jamkazam.local
|
|||
REACT_APP_CLIENT_BASE_URL=http://www.jamkazam.local:3000
|
||||
REACT_APP_API_BASE_URL=http://www.jamkazam.local:3000/api
|
||||
REACT_APP_BITBUCKET_BUILD_NUMBER=dev
|
||||
REACT_APP_BITBUCKET_COMMIT=dev
|
||||
REACT_APP_BITBUCKET_COMMIT=dev
|
||||
REACT_APP_ENV=development
|
||||
|
|
@ -2,4 +2,5 @@ HOST=beta.jamkazam.com
|
|||
PORT=4000
|
||||
REACT_APP_ORIGIN=jamkazam.com
|
||||
REACT_APP_CLIENT_BASE_URL=https://www.jamkazam.com
|
||||
REACT_APP_API_BASE_URL=https://www.jamkazam.com/api
|
||||
REACT_APP_API_BASE_URL=https://www.jamkazam.com/api
|
||||
REACT_APP_ENV=production
|
||||
|
|
@ -2,4 +2,5 @@ HOST=beta.staging.jamkazam.com
|
|||
PORT=4000
|
||||
REACT_APP_ORIGIN=staging.jamkazam.com
|
||||
REACT_APP_CLIENT_BASE_URL=https://staging.jamkazam.com
|
||||
REACT_APP_API_BASE_URL=https://staging.jamkazam.com/api
|
||||
REACT_APP_API_BASE_URL=https://staging.jamkazam.com/api
|
||||
REACT_APP_ENV=staging
|
||||
|
|
@ -10,7 +10,7 @@ describe('Profile update', () => {
|
|||
cy.stubAuthenticate({ ...currentUser });
|
||||
cy.intercept('GET', /\S+\/users\/\S+\/profile/, {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
body: {
|
||||
first_name: 'David',
|
||||
last_name: 'Wilson',
|
||||
name: 'David Wilson',
|
||||
|
|
@ -19,8 +19,7 @@ describe('Profile update', () => {
|
|||
country: 'US',
|
||||
location: 'Barstow, CA',
|
||||
photo_url: null,
|
||||
biography:
|
||||
'This is the musician biography. It is a long form text.',
|
||||
biography: 'This is the musician biography. It is a long form text.',
|
||||
virtual_band: true,
|
||||
virtual_band_commitment: 2,
|
||||
traditional_band: false,
|
||||
|
|
@ -75,47 +74,53 @@ describe('Profile update', () => {
|
|||
priority: 1,
|
||||
instrument_id: 'banjo'
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
}).as('getProfile');
|
||||
|
||||
|
||||
cy.intercept('PUT', /\S+\/users\/\S+/, {
|
||||
statusCode: 200
|
||||
}).as('updateProfile');
|
||||
|
||||
cy.visit('/profile');
|
||||
});
|
||||
|
||||
it('should render the profile form with persisted data', () => {
|
||||
// Assert that the profile form is rendered
|
||||
cy.wait('@getProfile');
|
||||
cy.get('[data-testid=edit_profile_form]').within($form => {
|
||||
cy.get('[data-testid=firstName]').should('have.value', 'David');
|
||||
cy.get('[data-testid=lastName]').should('have.value', 'Wilson');
|
||||
cy.get("[data-testid=biography]").should('contain', 'This is the musician biography. It is a long form text');
|
||||
|
||||
cy.get('[data-testid=biography]').should('contain', 'This is the musician biography. It is a long form text');
|
||||
|
||||
cy.get('[data-testid=subscribeEmail]').should('be.checked');
|
||||
cy.get('[data-testid=virtualBand]').should('be.checked');
|
||||
cy.get('[data-testid=traditionalBand]').should('not.be.checked');
|
||||
cy.get('[data-testid=cowriting]').should('be.checked');
|
||||
cy.get('[data-testid=instruments] input:checked').should('have.length', 4);
|
||||
cy.get('[data-testid=genres] input:checked').should('have.length', 3);
|
||||
} );
|
||||
});
|
||||
});
|
||||
|
||||
it('should update the profile', () => {
|
||||
it.only('should update the profile', () => {
|
||||
// Update the profile form
|
||||
|
||||
cy.get('[data-testid=edit_profile_form]').within($form => {
|
||||
// Update the first name
|
||||
cy.get('[data-testid=firstName]').clear().type('Seth');
|
||||
// Update the last name
|
||||
cy.get('[data-testid=lastName]').clear().type('Call');
|
||||
cy.get('[data-testid=firstName]')
|
||||
.clear()
|
||||
.type('Seth');
|
||||
|
||||
cy.wait(2000);
|
||||
// Update the last name
|
||||
cy.get('[data-testid=lastName]')
|
||||
.clear()
|
||||
.type('Call');
|
||||
|
||||
cy.wait(2000);
|
||||
});
|
||||
|
||||
cy.reload();
|
||||
|
||||
cy.get('[data-testid=firstName]').should('have.value', 'Seth');
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,55 +23,55 @@ describe('JamTracks Page', () => {
|
|||
cy.get('input[type="search"]').should('exist');
|
||||
});
|
||||
|
||||
describe.only('search artists', () => {
|
||||
describe('search', () => {
|
||||
beforeEach(() => {
|
||||
//http://www.jamkazam.local:3000/api/jamtracks/autocomplete?match=ac+&limit=5
|
||||
// cy.intercept('GET', /S+\/jamtracks\/autocomplete\?match=ac\S+/, {
|
||||
// body: [{ artists: [{ original_artist: 'AC DC' }], songs: [] }]
|
||||
// }).as('getJamTracksAutoComplete');
|
||||
cy.intercept('GET', /\S+\/jamtracks\?limit=100/, { fixture: 'jamtracks' }).as('getJamTracks');
|
||||
cy.intercept('GET', /\S+\/jamtracks\?per_page=10\&page=1\&\S+/, { fixture: 'jamtracks_page1' }).as('getJamTracks_page1');
|
||||
cy.intercept('GET', /\S+\/jamtracks\?per_page=10\&page=2\&\S+/, { fixture: 'jamtracks_page2' }).as('getJamTracks_page2');
|
||||
cy.intercept('GET', /\S+\/jamtracks\?per_page=10\&page=3\&\S+/, { fixture: 'jamtracks_page3' }).as('getJamTracks_page3');
|
||||
cy.intercept('POST', /\S+\/shopping_carts\/add_jamtrack/, {
|
||||
body: { success: true }
|
||||
}).as('addJamTrackToCart');
|
||||
})
|
||||
|
||||
it('should display the JamTracks', () => {
|
||||
cy.get('input[type="search"]').type('ba{enter}');
|
||||
cy.wait('@getJamTracks_page1');
|
||||
cy.contains('Search Results: JamTracks including "ba"');
|
||||
cy.get('[data-testid=jamtracks-table] tbody tr:first .track-name-col').should('contain', 'Back in Black by AC DC');
|
||||
cy.get('[data-testid=jamtracks-table] tbody tr:first .track-tracks-col .jamtrack-track:visible').should('have.length', 6);
|
||||
cy.get('[data-testid=jamtracks-table] tbody tr:first .track-tracks-col').contains('Show all tracks').click();
|
||||
cy.get('[data-testid=jamtracks-table] tbody tr:first .track-tracks-col .jamtrack-track:visible').should('have.length', 10);
|
||||
cy.get('[data-testid=jamtracks-table] tbody tr:first .track-tracks-col').contains('Show fewer tracks').click();
|
||||
cy.get('[data-testid=jamtracks-table] tbody tr:first .track-tracks-col .jamtrack-track:visible').should('have.length', 6);
|
||||
|
||||
//load more
|
||||
cy.get('button').contains('Load More').click();
|
||||
cy.wait('@getJamTracks_page2');
|
||||
|
||||
//load more
|
||||
cy.get('button').contains('Load More').click();
|
||||
cy.wait('@getJamTracks_page3');
|
||||
|
||||
cy.get('[data-testid=jamtracks-table] tbody tr').should('have.length', 30);
|
||||
|
||||
//no more pages
|
||||
cy.get('[data-testid=moreBtn]').should('not.exist');
|
||||
});
|
||||
|
||||
it('let user to purchase a JamTrack', () => {
|
||||
cy.get('input[type="search"]').type('ba{enter}');
|
||||
cy.wait('@getJamTracks_page1');
|
||||
cy.get('[data-testid=jamtracks-table] tbody tr').eq(2).find('.purchase-button-col button').should('contain', 'Add to Cart').click();
|
||||
cy.wait('@addJamTrackToCart');
|
||||
cy.location('pathname').should('eq', '/shopping-cart')
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should display the JamTracks', () => {
|
||||
cy.get('input[type="search"]').type('ba{enter}');
|
||||
cy.wait('@getJamTracks');
|
||||
cy.contains('Search Results: JamTracks including "ba"');
|
||||
cy.get('[data-testid=jamtracks-table] .track-name-col').should('contain', 'Back in Black by AC DC');
|
||||
cy.get('[data-testid=jamtracks-table] .track-tracks-col .jamtrack-track:visible').should('have.length', 6);
|
||||
cy.get('[data-testid=jamtracks-table] .track-tracks-col').contains('Show all tracks').click();
|
||||
cy.get('[data-testid=jamtracks-table] .track-tracks-col .jamtrack-track:visible').should('have.length', 10);
|
||||
cy.get('[data-testid=jamtracks-table] .track-tracks-col').contains('Show fewer tracks').click();
|
||||
cy.get('[data-testid=jamtracks-table] .track-tracks-col .jamtrack-track:visible').should('have.length', 6);
|
||||
});
|
||||
});
|
||||
|
||||
// it('should display the JamTracks list', () => {
|
||||
// cy.get('table').should('exist');
|
||||
// });
|
||||
|
||||
// it('should display the JamTracks pagination', () => {
|
||||
// cy.get('button').contains('Next').should('exist');
|
||||
// });
|
||||
|
||||
// it('should display the JamTracks preview', () => {
|
||||
// cy.get('table').find('tr').first().find('td').eq(1).find('button').click();
|
||||
// cy.get('table').find('tr').first().find('td').eq(1).find('button').should('contain', 'Preview');
|
||||
// });
|
||||
|
||||
// it('should display the JamTracks purchase button', () => {
|
||||
// cy.get('table').find('tr').first().find('td').eq(2).find('button').click();
|
||||
// cy.get('table').find('tr').first().find('td').eq(2).find('button').should('contain', 'Add to Cart');
|
||||
// });
|
||||
|
||||
// it('should display the JamTracks artist search results', () => {
|
||||
// cy.get('input[type="search"]').type('artist');
|
||||
// cy.get('ul').find('li').first().click();
|
||||
// cy.get('ul').should('not.exist');
|
||||
// });
|
||||
|
||||
// it('should display the JamTracks artist search results', () => {
|
||||
// cy.get('input[type="search"]').type('artist');
|
||||
// cy.get('ul').find('li').first().click();
|
||||
// cy.get('ul').should('not.exist');
|
||||
// });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
/// <reference types="cypress" />
|
||||
|
||||
import makeFakeUser from '../../factories/user';
|
||||
|
||||
describe('JamTracks Page', () => {
|
||||
beforeEach(() => {
|
||||
const currentUser = makeFakeUser();
|
||||
cy.stubAuthenticate({ id: currentUser.id });
|
||||
cy.visit('/my-jamtracks');
|
||||
});
|
||||
|
||||
it('should display the My JamTracks page', () => {
|
||||
cy.get('.card-header h5').should('contain', 'My JamTracks');
|
||||
});
|
||||
|
||||
it('should display the search bar', () => {
|
||||
cy.get('input[type="search"]').should('exist');
|
||||
});
|
||||
|
||||
describe('filter', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', /\S+\/jamtracks\/purchased\?page=1\&\S+/, { fixture: 'my_jamtracks_page1' }).as('getMyJamTracks_page1');
|
||||
});
|
||||
|
||||
it('should display the JamTracks', () => {
|
||||
cy.get('input[type="search"]').type('ba');
|
||||
cy.wait('@getMyJamTracks_page1');
|
||||
cy.get('[data-testid=myJamTrackList]').should('contain', 'Back in Black by AC DC');
|
||||
});
|
||||
|
||||
it('clicking on a JamTrack should navigate to the JamTrack page', () => {
|
||||
cy.get('input[type="search"]').type('ba');
|
||||
cy.wait('@getMyJamTracks_page1');
|
||||
cy.get('[data-testid=myJamTrackList] a').first().click();
|
||||
cy.url().should('include', '/jamtracks/1');
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
|
@ -1,120 +1,198 @@
|
|||
/// <reference types="cypress" />
|
||||
|
||||
describe("Top Navigation", () => {
|
||||
|
||||
describe('Top Navigation', () => {
|
||||
const showSubscribeToUpdates = () => {
|
||||
cy.contains('Keep JamKazam Improving').should('exist')
|
||||
cy.contains('Subscribe').should('exist')
|
||||
}
|
||||
cy.contains('Keep JamKazam Improving').should('exist');
|
||||
cy.contains('Subscribe').should('exist');
|
||||
};
|
||||
|
||||
const showProfileDropdown = () => {
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('exist')
|
||||
cy.contains("Peter Pan").should('exist')
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('exist');
|
||||
cy.contains('Peter Pan').should('exist');
|
||||
//cy.contains("My Profile").should('exist')
|
||||
cy.contains("Sign Out").should('exist')
|
||||
}
|
||||
cy.contains('Sign Out').should('exist');
|
||||
};
|
||||
|
||||
describe("when user has not logged in", () => {
|
||||
describe('when user has not logged in', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubUnauthenticate()
|
||||
cy.stubUnauthenticate();
|
||||
});
|
||||
|
||||
it('shows homepage', () => {
|
||||
cy.visit('/')
|
||||
cy.contains('Home').should('exist')
|
||||
showSubscribeToUpdates()
|
||||
})
|
||||
|
||||
it("not allowed to protected page", () => {
|
||||
cy.visit('/friends')
|
||||
cy.url().should('include', '/authentication/basic/login')
|
||||
cy.contains("Sign in")
|
||||
cy.get('button').should('have.text', 'Sign in')
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('not.exist')
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
cy.contains('Home').should('exist');
|
||||
showSubscribeToUpdates();
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
describe("when user has logged in", () => {
|
||||
|
||||
it('not allowed to protected page', () => {
|
||||
cy.visit('/friends');
|
||||
cy.wait('@getAppFeatures');
|
||||
cy.url().should('include', '/authentication/basic/login');
|
||||
cy.contains('Sign in');
|
||||
cy.get('button').should('have.text', 'Sign in');
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user has logged in', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate()
|
||||
cy.visit('/')
|
||||
cy.stubAuthenticate();
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
});
|
||||
|
||||
it("shows user dropdown", () => {
|
||||
showSubscribeToUpdates()
|
||||
showProfileDropdown()
|
||||
})
|
||||
it('shows user dropdown', () => {
|
||||
showSubscribeToUpdates();
|
||||
showProfileDropdown();
|
||||
});
|
||||
|
||||
it('sign out', () => {
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').contains('Peter Pan').click()
|
||||
cy.stubUnauthenticate()
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').contains('Sign Out').click()
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('not.exist')
|
||||
cy.contains("Home")
|
||||
})
|
||||
})
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]')
|
||||
.contains('Peter Pan')
|
||||
.click();
|
||||
cy.stubUnauthenticate();
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]')
|
||||
.contains('Sign Out')
|
||||
.click();
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('not.exist');
|
||||
cy.contains('Home');
|
||||
});
|
||||
});
|
||||
|
||||
describe('header notifications', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate()
|
||||
cy.intercept('GET', /\S+\/notifications/, { fixture: 'notifications'} )
|
||||
cy.stubAuthenticate();
|
||||
cy.intercept('GET', /\S+\/notifications/, { fixture: 'notifications' });
|
||||
cy.intercept('GET', /\S+\/profile\S+/, { fixture: 'person' });
|
||||
cy.visit('/')
|
||||
})
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
});
|
||||
|
||||
it('shows notifications', () => {
|
||||
cy.get('[data-testid=notificationDropdown]').should('not.be.visible')
|
||||
cy.get('.notification-indicator').click()
|
||||
cy.get('[data-testid=notificationDropdown]').should('be.visible')
|
||||
cy.get('[data-testid=notificationDropdown] .list-group-item').should('have.length', 3)
|
||||
cy.get('[data-testid=notificationDropdown]').contains('View all').click() //view all notifications
|
||||
cy.url().should('include', '/notifications')
|
||||
})
|
||||
})
|
||||
cy.get('[data-testid=notificationDropdown]').should('not.be.visible');
|
||||
cy.get('.notification-indicator').click();
|
||||
cy.get('[data-testid=notificationDropdown]').should('be.visible');
|
||||
cy.get('[data-testid=notificationDropdown] .list-group-item').should('have.length', 3);
|
||||
cy.get('[data-testid=notificationDropdown]')
|
||||
.contains('View all')
|
||||
.click(); //view all notifications
|
||||
cy.url().should('include', '/notifications');
|
||||
});
|
||||
});
|
||||
|
||||
describe('locale switch', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate()
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it("translate", () => {
|
||||
cy.get('.card-header').contains('Home')
|
||||
cy.get('[data-testid=langSwitch]').contains('ES').click()
|
||||
cy.get('.card-header').contains('Página de inicio')
|
||||
cy.get('.card-header').should('not.contain', 'Home')
|
||||
cy.get('[data-testid=langSwitch]').contains('EN').click()
|
||||
cy.get('.card-header').contains('Home')
|
||||
cy.get('.card-header').should('not.contain', 'Página de inicio')
|
||||
})
|
||||
})
|
||||
|
||||
describe('left side navigation', () => {
|
||||
beforeEach(() => {
|
||||
cy.viewport('macbook-13');
|
||||
cy.stubAuthenticate()
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('shows all main and sub menu items opened by default', () => {
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Sessions')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Create Session')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Browse Current Sessions')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('View Session History')
|
||||
})
|
||||
|
||||
it.only('toggles only one menu on click', () => {
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Sessions').click()
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Create Session').should('not.visible')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Browse Current Sessions').should('not.visible')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('View Session History').should('not.visible')
|
||||
//the Friends menu is not toggled
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Friends')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('My Friends')
|
||||
})
|
||||
})
|
||||
cy.stubAuthenticate();
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
});
|
||||
|
||||
it('translate', () => {
|
||||
cy.get('.card-header').contains('Home');
|
||||
cy.get('[data-testid=langSwitch]')
|
||||
.contains('ES')
|
||||
.click();
|
||||
cy.get('.card-header').contains('Página de inicio');
|
||||
cy.get('.card-header').should('not.contain', 'Home');
|
||||
cy.get('[data-testid=langSwitch]')
|
||||
.contains('EN')
|
||||
.click();
|
||||
cy.get('.card-header').contains('Home');
|
||||
cy.get('.card-header').should('not.contain', 'Página de inicio');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Side Navigation', () => {
|
||||
describe('backend returns empty set of app features', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate();
|
||||
cy.viewport('macbook-13');
|
||||
});
|
||||
|
||||
it('shows all menu items', () => {
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Home');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Sessions');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Create Session');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Friends');
|
||||
});
|
||||
|
||||
it('toggles only one menu on click', () => {
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
cy.get('[data-testid=verticalNavigation]')
|
||||
.contains('Sessions')
|
||||
.click();
|
||||
cy.get('[data-testid=verticalNavigation]')
|
||||
.contains('Create Session')
|
||||
.should('not.visible');
|
||||
cy.get('[data-testid=verticalNavigation]')
|
||||
.contains('Browse Sessions')
|
||||
.should('not.visible');
|
||||
cy.get('[data-testid=verticalNavigation]')
|
||||
.contains('Session History')
|
||||
.should('not.visible');
|
||||
//the Friends menu is not toggled
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Friends');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('My Friends');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('backend returns app features', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate();
|
||||
cy.viewport('macbook-13');
|
||||
cy.intercept('GET', /\S+\/app_features/, {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
{
|
||||
handle: '/sessions',
|
||||
is_enabled: true,
|
||||
feature_type: 'page',
|
||||
env: 'development'
|
||||
},
|
||||
{
|
||||
handle: '/sessions/new',
|
||||
is_enabled: false,
|
||||
feature_type: 'page',
|
||||
env: 'development'
|
||||
},
|
||||
{
|
||||
handle: '/sessions/history',
|
||||
is_enabled: true,
|
||||
feature_type: 'page',
|
||||
env: 'production'
|
||||
},
|
||||
{
|
||||
handle: '/friends',
|
||||
is_enabled: true,
|
||||
feature_type: 'page',
|
||||
env: 'development'
|
||||
},
|
||||
{
|
||||
handle: '/friends/my',
|
||||
is_enabled: true,
|
||||
feature_type: 'page',
|
||||
env: 'development'
|
||||
}
|
||||
]
|
||||
}).as('getAppFeatures');
|
||||
});
|
||||
|
||||
it('shows only enabled menu items', () => {
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Friends');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Sessions');
|
||||
cy.get('[data-testid=verticalNavigation]').should('not.include.text', 'Create Session')
|
||||
cy.get('[data-testid=verticalNavigation]').should('not.include.text', 'Session History')
|
||||
});
|
||||
|
||||
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,191 +0,0 @@
|
|||
{
|
||||
"next": null,
|
||||
"count": 1,
|
||||
"jamtracks": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Back in Black",
|
||||
"description": "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the AC DC song \"Back in Black\".",
|
||||
"recording_type": "Cover",
|
||||
"original_artist": "AC DC",
|
||||
"songwriter": null,
|
||||
"publisher": null,
|
||||
"sales_region": "Worldwide",
|
||||
"price": "1.99",
|
||||
"version": "0",
|
||||
"duration": 221,
|
||||
"year": null,
|
||||
"plan_code": "jamtrack-acdc-backinblack",
|
||||
"allow_free": true,
|
||||
"download_price": "2.99",
|
||||
"upgrade_price": "1.0",
|
||||
"tracks": [
|
||||
{
|
||||
"id": "103dea4d-f2a3-4414-8efe-d2ca378dda60",
|
||||
"part": "Master Mix",
|
||||
"instrument": {
|
||||
"id": "computer",
|
||||
"description": "Computer",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Master",
|
||||
"position": 1000,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Master%20Mix-44100-preview-e9a5a63f34b4d523ee1842fff31f88ce.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Master%20Mix-44100-preview-25fcba7ace7086e3cb6b97d7e33ba72e.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Master%20Mix-44100-preview-9f0b072ed9f4b546e170fcdfb302137e.mp3"
|
||||
},
|
||||
{
|
||||
"id": "2755cbdd-0476-4f3b-9ba1-e2da561ddb4e",
|
||||
"part": "Lead",
|
||||
"instrument": {
|
||||
"id": "voice",
|
||||
"description": "Voice",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 1,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Vocal%20-%20Lead-44100-preview-d35c328fc3936dad9a79fe102dc72950.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Vocal%20-%20Lead-44100-preview-b97b37651eae352fae3b3060918c7bcb.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Vocal%20-%20Lead-44100-preview-d35c328fc3936dad9a79fe102dc72950.aac"
|
||||
},
|
||||
{
|
||||
"id": "0db7c4e1-5e8d-43fe-bd35-98acd8f68b26",
|
||||
"part": "Drums",
|
||||
"instrument": {
|
||||
"id": "drums",
|
||||
"description": "Drums",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 2,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Drums-44100-preview-03aadceb966caf40b96334bdd00234f6.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Drums-44100-preview-854914e3e0d6fdc5f0794325b0ecaead.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Drums-44100-preview-03aadceb966caf40b96334bdd00234f6.aac"
|
||||
},
|
||||
{
|
||||
"id": "2cc79ab6-dab8-4905-85e6-0df5f8e087f1",
|
||||
"part": "Bass",
|
||||
"instrument": {
|
||||
"id": "bass guitar",
|
||||
"description": "Bass Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 3,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Bass-44100-preview-61c334ac87f811bd010ed3a910764c2e.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Bass-44100-preview-4066dafd7b72e9993b0c0fe1dba2b332.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Bass-44100-preview-61c334ac87f811bd010ed3a910764c2e.aac"
|
||||
},
|
||||
{
|
||||
"id": "ed1d3487-3b32-442f-9c76-8a36fe3bb643",
|
||||
"part": "Solo",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 4,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Solo-44100-preview-e9fe8572a9ac1022762642cbd92b3c34.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Solo-44100-preview-5fb058042254206cfa9fb4dcb0310b2c.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Solo-44100-preview-e9fe8572a9ac1022762642cbd92b3c34.aac"
|
||||
},
|
||||
{
|
||||
"id": "f4ce7c91-7542-4e03-8fc2-68b31683d33e",
|
||||
"part": "Rhythm 1",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 5,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%201-44100-preview-6b498479823d4131a01fa535817d5eab.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%201-44100-preview-f4cbb31dbde3e1a3e6012730a7e0e10f.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%201-44100-preview-6b498479823d4131a01fa535817d5eab.aac"
|
||||
},
|
||||
{
|
||||
"id": "2d96c7ec-59f1-4d56-8a7f-7f4c75a0ccef",
|
||||
"part": "Rhythm 2",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 6,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%202-44100-preview-a626d7c632560f6737e1b6024141289e.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%202-44100-preview-06a0e5af451f001f3465992efcd34ec0.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%202-44100-preview-a626d7c632560f6737e1b6024141289e.aac"
|
||||
},
|
||||
{
|
||||
"id": "fce018ca-c897-4137-aa10-ef56a8e1831f",
|
||||
"part": "Intro Scrapes",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 7,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Intro%20Scrapes-44100-preview-0ddfaa7154e9ba35d05d60477d5dd3e9.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Intro%20Scrapes-44100-preview-f53ce3c5f9dcf81af51560f52635fbb0.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Intro%20Scrapes-44100-preview-0ddfaa7154e9ba35d05d60477d5dd3e9.aac"
|
||||
},
|
||||
{
|
||||
"id": "c9b3e0a8-4db0-4d0f-9769-398a6d56506e",
|
||||
"part": "Main",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 8,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Main-44100-preview-234a224f75a97d7ff8f55442ece6fcde.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Main-44100-preview-828c9691f5435dea1c90182fa2618c9b.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Main-44100-preview-234a224f75a97d7ff8f55442ece6fcde.aac"
|
||||
},
|
||||
{
|
||||
"id": "28c3df07-2a88-45a9-9ae6-3399a5d2eb20",
|
||||
"part": "Sound FX",
|
||||
"instrument": {
|
||||
"id": "computer",
|
||||
"description": "Computer",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 9,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Sound%20Effects-44100-preview-6c859c73036cd55bceb65f19f2d2f2f3.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Sound%20Effects-44100-preview-f840d8df4c7388f776477139025ee712.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Sound%20Effects-44100-preview-6c859c73036cd55bceb65f19f2d2f2f3.aac"
|
||||
}
|
||||
],
|
||||
"licensor": null,
|
||||
"genres": ["Rock", "Pop"],
|
||||
"added_cart": true,
|
||||
"can_download": true,
|
||||
"purchased": true
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,562 @@
|
|||
|
||||
{
|
||||
"next": 10,
|
||||
"count": 3756,
|
||||
"jamtracks": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Back in Black",
|
||||
"description": "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the AC DC song \"Back in Black\".",
|
||||
"recording_type": "Cover",
|
||||
"original_artist": "AC DC",
|
||||
"songwriter": null,
|
||||
"publisher": null,
|
||||
"sales_region": "Worldwide",
|
||||
"price": "1.99",
|
||||
"version": "0",
|
||||
"duration": 221,
|
||||
"year": null,
|
||||
"plan_code": "jamtrack-acdc-backinblack",
|
||||
"allow_free": true,
|
||||
"download_price": "2.99",
|
||||
"upgrade_price": "1.0",
|
||||
"tracks": [
|
||||
{
|
||||
"id": "103dea4d-f2a3-4414-8efe-d2ca378dda60",
|
||||
"part": "Master Mix",
|
||||
"instrument": {
|
||||
"id": "computer",
|
||||
"description": "Computer",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Master",
|
||||
"position": 1000,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Master%20Mix-44100-preview-e9a5a63f34b4d523ee1842fff31f88ce.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Master%20Mix-44100-preview-25fcba7ace7086e3cb6b97d7e33ba72e.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Master%20Mix-44100-preview-9f0b072ed9f4b546e170fcdfb302137e.mp3"
|
||||
},
|
||||
{
|
||||
"id": "2755cbdd-0476-4f3b-9ba1-e2da561ddb4e",
|
||||
"part": "Lead",
|
||||
"instrument": {
|
||||
"id": "voice",
|
||||
"description": "Voice",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 1,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Vocal%20-%20Lead-44100-preview-d35c328fc3936dad9a79fe102dc72950.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Vocal%20-%20Lead-44100-preview-b97b37651eae352fae3b3060918c7bcb.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Vocal%20-%20Lead-44100-preview-d35c328fc3936dad9a79fe102dc72950.aac"
|
||||
},
|
||||
{
|
||||
"id": "0db7c4e1-5e8d-43fe-bd35-98acd8f68b26",
|
||||
"part": "Drums",
|
||||
"instrument": {
|
||||
"id": "drums",
|
||||
"description": "Drums",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 2,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Drums-44100-preview-03aadceb966caf40b96334bdd00234f6.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Drums-44100-preview-854914e3e0d6fdc5f0794325b0ecaead.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Drums-44100-preview-03aadceb966caf40b96334bdd00234f6.aac"
|
||||
},
|
||||
{
|
||||
"id": "2cc79ab6-dab8-4905-85e6-0df5f8e087f1",
|
||||
"part": "Bass",
|
||||
"instrument": {
|
||||
"id": "bass guitar",
|
||||
"description": "Bass Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 3,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Bass-44100-preview-61c334ac87f811bd010ed3a910764c2e.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Bass-44100-preview-4066dafd7b72e9993b0c0fe1dba2b332.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Bass-44100-preview-61c334ac87f811bd010ed3a910764c2e.aac"
|
||||
},
|
||||
{
|
||||
"id": "ed1d3487-3b32-442f-9c76-8a36fe3bb643",
|
||||
"part": "Solo",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 4,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Solo-44100-preview-e9fe8572a9ac1022762642cbd92b3c34.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Solo-44100-preview-5fb058042254206cfa9fb4dcb0310b2c.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Solo-44100-preview-e9fe8572a9ac1022762642cbd92b3c34.aac"
|
||||
},
|
||||
{
|
||||
"id": "f4ce7c91-7542-4e03-8fc2-68b31683d33e",
|
||||
"part": "Rhythm 1",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 5,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%201-44100-preview-6b498479823d4131a01fa535817d5eab.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%201-44100-preview-f4cbb31dbde3e1a3e6012730a7e0e10f.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%201-44100-preview-6b498479823d4131a01fa535817d5eab.aac"
|
||||
},
|
||||
{
|
||||
"id": "2d96c7ec-59f1-4d56-8a7f-7f4c75a0ccef",
|
||||
"part": "Rhythm 2",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 6,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%202-44100-preview-a626d7c632560f6737e1b6024141289e.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%202-44100-preview-06a0e5af451f001f3465992efcd34ec0.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%202-44100-preview-a626d7c632560f6737e1b6024141289e.aac"
|
||||
},
|
||||
{
|
||||
"id": "fce018ca-c897-4137-aa10-ef56a8e1831f",
|
||||
"part": "Intro Scrapes",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 7,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Intro%20Scrapes-44100-preview-0ddfaa7154e9ba35d05d60477d5dd3e9.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Intro%20Scrapes-44100-preview-f53ce3c5f9dcf81af51560f52635fbb0.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Intro%20Scrapes-44100-preview-0ddfaa7154e9ba35d05d60477d5dd3e9.aac"
|
||||
},
|
||||
{
|
||||
"id": "c9b3e0a8-4db0-4d0f-9769-398a6d56506e",
|
||||
"part": "Main",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 8,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Main-44100-preview-234a224f75a97d7ff8f55442ece6fcde.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Main-44100-preview-828c9691f5435dea1c90182fa2618c9b.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Electric%20Guitar%20-%20Main-44100-preview-234a224f75a97d7ff8f55442ece6fcde.aac"
|
||||
},
|
||||
{
|
||||
"id": "28c3df07-2a88-45a9-9ae6-3399a5d2eb20",
|
||||
"part": "Sound FX",
|
||||
"instrument": {
|
||||
"id": "computer",
|
||||
"description": "Computer",
|
||||
"created_at": "2021-02-02T23:16:46.168Z",
|
||||
"updated_at": "2021-02-02T23:16:46.168Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 9,
|
||||
"preview_mp3_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Sound%20Effects-44100-preview-6c859c73036cd55bceb65f19f2d2f2f3.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Sound%20Effects-44100-preview-f840d8df4c7388f776477139025ee712.ogg",
|
||||
"preview_aac_url": "https://jamkazam-dev-public.s3.amazonaws.com/jam_track_previews/AC%20DC/Back%20in%20Black/Back%20in%20Black%20Stem%20-%20Sound%20Effects-44100-preview-6c859c73036cd55bceb65f19f2d2f2f3.aac"
|
||||
}
|
||||
],
|
||||
"licensor": null,
|
||||
"genres": ["Rock", "Pop"],
|
||||
"added_cart": true,
|
||||
"can_download": true,
|
||||
"purchased": true
|
||||
},
|
||||
{
|
||||
"id": "531",
|
||||
"name": "1234",
|
||||
"description": "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the Feist song \"1234\".",
|
||||
"recording_type": "Cover",
|
||||
"original_artist": "Feist",
|
||||
"songwriter": null,
|
||||
"publisher": null,
|
||||
"sales_region": "Worldwide",
|
||||
"price": "1.99",
|
||||
"version": "1",
|
||||
"duration": 184,
|
||||
"year": 2007,
|
||||
"plan_code": "jamtrack-feist-1234",
|
||||
"allow_free": true,
|
||||
"download_price": "2.99",
|
||||
"upgrade_price": "1.0",
|
||||
"tracks": [
|
||||
{
|
||||
"id": "b834ce9c-2624-4977-a079-0b1b5d90ad6e",
|
||||
"part": "Clicktrack",
|
||||
"instrument": {
|
||||
"id": "computer",
|
||||
"description": "Computer",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Click",
|
||||
"position": 10000,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234%20-%20Tency%20Music/1234%20Clicktrack-44100-preview-90bed87ea6402ab0f8d283ffe094c95f.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234%20-%20Tency%20Music/1234%20Clicktrack-44100-preview-7198bae1519e40827aff0bd704e2066b.ogg",
|
||||
"preview_aac_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234%20-%20Tency%20Music/1234%20Clicktrack-44100-preview-90bed87ea6402ab0f8d283ffe094c95f.aac"
|
||||
},
|
||||
{
|
||||
"id": "b5a67d64-f94f-453e-9dab-691304275bc2",
|
||||
"part": "Master Mix",
|
||||
"instrument": {
|
||||
"id": "computer",
|
||||
"description": "Computer",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Master",
|
||||
"position": 1000,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1%202%203%204%20Master%20Mix-44100-preview-5fe2a923a614f17dd9c6440b65c1e884.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1%202%203%204%20Master%20Mix-44100-preview-aaefd823d7deaa1bc862eb8ec1e53fe3.ogg",
|
||||
"preview_aac_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1%202%203%204%20Master%20Mix-44100-preview-6d3edecd174080e88e611984969f1b2d.aac"
|
||||
},
|
||||
{
|
||||
"id": "27353eb1-9f2b-487b-9047-93f49446b4db",
|
||||
"part": "Lead",
|
||||
"instrument": {
|
||||
"id": "voice",
|
||||
"description": "Voice",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 1,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Vocal%20-%20Lead-44100-preview-426d923380d10fbdf2d5a4e4108b0de2.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Vocal%20-%20Lead-44100-preview-30eaef3aea1499493cbad76c5c3c8a50.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "78736d23-e657-4922-b145-9f7db40bb36e",
|
||||
"part": "Backing",
|
||||
"instrument": {
|
||||
"id": "voice",
|
||||
"description": "Voice",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 2,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Vocal%20-%20Backing-44100-preview-c0fb7721f3c8cd3253c8c5a6d6b034dd.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Vocal%20-%20Backing-44100-preview-b7145437bbb6b01b566f10ef9009d001.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "1978c439-115b-4214-85a6-beec780310a1",
|
||||
"part": "Drums",
|
||||
"instrument": {
|
||||
"id": "drums",
|
||||
"description": "Drums",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 3,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Drums%20-%20Drums-44100-preview-da3d7e78b9a7b50bea7bcc8f13a7d4b0.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Drums%20-%20Drums-44100-preview-d320dc7600c6d9c08af19b813d6d8176.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "8457c55d-924d-4048-8c5f-46b5c7668552",
|
||||
"part": "Bass",
|
||||
"instrument": {
|
||||
"id": "bass guitar",
|
||||
"description": "Bass Guitar",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 4,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Bass%20Guitar%20-%20Bass-44100-preview-c4b1ce442a9645f6ead0f078afe48d3d.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Bass%20Guitar%20-%20Bass-44100-preview-8a0b0d664802015a219f3320d4d5c0cd.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "ca5d5597-1a19-45b1-9d1b-478f6b6c4b19",
|
||||
"part": "Piano",
|
||||
"instrument": {
|
||||
"id": "piano",
|
||||
"description": "Piano",
|
||||
"created_at": "2014-02-16T13:10:07.059Z",
|
||||
"updated_at": "2014-02-16T13:10:07.059Z",
|
||||
"popularity": 2
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 5,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Piano%20-%20Piano-44100-preview-208fb9e053bb445c1d55ec443809211c.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Piano%20-%20Piano-44100-preview-8940844b3972b39ad9b786805496bc1b.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "755c6c48-fc6c-4dc8-bbf2-9dc0d0a5f2a8",
|
||||
"part": "Acoustic",
|
||||
"instrument": {
|
||||
"id": "acoustic guitar",
|
||||
"description": "Acoustic Guitar",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 6,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Acoustic%20Guitar%20-%20Acoustic-44100-preview-11bde0963b7a833072a49c1096a0b6ef.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Acoustic%20Guitar%20-%20Acoustic-44100-preview-92c44a32feac702880796fd9d6d5f883.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "508bcbaa-4d29-44c0-8cdd-f57fd3fee717",
|
||||
"part": "Banjo",
|
||||
"instrument": {
|
||||
"id": "banjo",
|
||||
"description": "Banjo",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 2
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 7,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Banjo%20-%20Banjo-44100-preview-6ed9db0acb0616651e297b0c057f453d.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Banjo%20-%20Banjo-44100-preview-1e18e0b352e4b05fefa35ce7fd9c84a1.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "6ef8af53-6615-450c-b78b-4310fb0e0395",
|
||||
"part": "Strings",
|
||||
"instrument": {
|
||||
"id": "orchestra",
|
||||
"description": "Orchestra",
|
||||
"created_at": "2015-08-11T16:08:58.806Z",
|
||||
"updated_at": "2015-08-11T16:08:58.806Z",
|
||||
"popularity": 1
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 8,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Orchestra%20-%20Strings-44100-preview-7017822e5edaedbe91fea259cc29666c.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Orchestra%20-%20Strings-44100-preview-70f5d96ed08b1d6a625f21c8be3f691c.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "91427115-63be-49e6-adb1-078f4a7f7ae8",
|
||||
"part": "Trumpet",
|
||||
"instrument": {
|
||||
"id": "trumpet",
|
||||
"description": "Trumpet",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 2
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 9,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Trumpet%20-%20Trumpet-44100-preview-74d7ab8c5af40dc9768176df2afd691a.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Feist/1234/1234%20Stem%20-%20Trumpet%20-%20Trumpet-44100-preview-df093dfebc96d3c22c13ed8d14b44ed0.ogg",
|
||||
"preview_aac_url": null
|
||||
}
|
||||
],
|
||||
"licensor": { "id": "027d90a1-b126-4d5a-8af6-3167296dfb04", "name": "Tency Music" },
|
||||
"genres": ["Folk", "Alternative Rock"],
|
||||
"added_cart": false,
|
||||
"can_download": false,
|
||||
"purchased": false
|
||||
},
|
||||
{
|
||||
"id": "1437",
|
||||
"name": "18 And Life",
|
||||
"description": "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the Skid Row song \"18 And Life\".",
|
||||
"recording_type": "Cover",
|
||||
"original_artist": "Skid Row",
|
||||
"songwriter": null,
|
||||
"publisher": null,
|
||||
"sales_region": "Worldwide",
|
||||
"price": "1.99",
|
||||
"version": "1",
|
||||
"duration": 227,
|
||||
"year": 1989,
|
||||
"plan_code": "jamtrack-skidrow-18andlife",
|
||||
"allow_free": true,
|
||||
"download_price": "2.99",
|
||||
"upgrade_price": "1.0",
|
||||
"tracks": [
|
||||
{
|
||||
"id": "7c515f02-bebd-4fdd-b30a-81b72ec277b9",
|
||||
"part": "Clicktrack",
|
||||
"instrument": {
|
||||
"id": "computer",
|
||||
"description": "Computer",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Click",
|
||||
"position": 10000,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life%20-%20Tency%20Music/click-44100-preview-424086caaa67c89532635ef0970b2d75.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life%20-%20Tency%20Music/click-44100-preview-d30de7c1353826896110d3ce5f459b23.ogg",
|
||||
"preview_aac_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life%20-%20Tency%20Music/click-44100-preview-424086caaa67c89532635ef0970b2d75.aac"
|
||||
},
|
||||
{
|
||||
"id": "db0a34e4-71e9-4342-b04a-e7c7f068b4fb",
|
||||
"part": "Master Mix",
|
||||
"instrument": {
|
||||
"id": "computer",
|
||||
"description": "Computer",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Master",
|
||||
"position": 1000,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/18%20And%20Life%20Master%20Mix-44100-preview-5bf5b2872e898dc43875f829e238b62b.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/18%20And%20Life%20Master%20Mix-44100-preview-3ba1eb2b8ab936212cc299cfb8db34ac.ogg",
|
||||
"preview_aac_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/18%20And%20Life%20Master%20Mix-44100-preview-146e9a850f83a2f76dc31dc9489dc3aa.aac"
|
||||
},
|
||||
{
|
||||
"id": "cc237bfd-5d87-413b-a460-55d17594f785",
|
||||
"part": "Lead",
|
||||
"instrument": {
|
||||
"id": "voice",
|
||||
"description": "Voice",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 1,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/ld-44100-preview-04a953ee8607a97e8533ea5adf4560a1.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/ld-44100-preview-3486ef50eb889b71208040a0a13de52f.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "a182cf33-1d61-41a8-8e9b-fd23d501885c",
|
||||
"part": "Drums",
|
||||
"instrument": {
|
||||
"id": "drums",
|
||||
"description": "Drums",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 2,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/drums-44100-preview-7ec143f7aa0dc019a2af48c6861c400b.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/drums-44100-preview-6fd8eacb6a8bd35a85e0d4a3a8ec120c.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "406c1f87-c6c1-406f-85a9-05768267ff46",
|
||||
"part": "Bass",
|
||||
"instrument": {
|
||||
"id": "bass guitar",
|
||||
"description": "Bass Guitar",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 3,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/bass-44100-preview-37326a28b8d55f4b61a00eb7406a4da9.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/bass-44100-preview-ff2e87a89ae7693fa8f15a3d065c5833.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "fa46fcd7-0647-4f94-93fd-bddb403abc8b",
|
||||
"part": "Lead",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 4,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/18%20And%20Life%20Stem%20-%20Electric%20Guitar%20-%20Lead-44100-preview-7b8ac822d2a0ccf341c4607919e5125a.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/18%20And%20Life%20Stem%20-%20Electric%20Guitar%20-%20Lead-44100-preview-3d13650573ebe20f939a7e2d661f9fab.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "42e6c71e-715c-48b9-b000-fb7fdabd3fe3",
|
||||
"part": "Rhythm Distorted",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 5,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/18%20And%20Life%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%20Distorted-44100-preview-4f0312e6d429fa211c873ecd671c2629.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/18%20And%20Life%20Stem%20-%20Electric%20Guitar%20-%20Rhythm%20Distorted-44100-preview-36e75c706ff27930a54be1159afafd9a.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "a16a69b1-2b89-4bca-8039-bb927c7f517f",
|
||||
"part": "Arpeggios Clean",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 6,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/18%20And%20Life%20Stem%20-%20Electric%20Guitar%20-%20Arpeggios%20Clean-44100-preview-f1f3df4288f9ac9bd8183303099f1a40.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/18%20And%20Life%20Stem%20-%20Electric%20Guitar%20-%20Arpeggios%20Clean-44100-preview-f8da9761dbed9afd32d636fb0ba2f757.ogg",
|
||||
"preview_aac_url": null
|
||||
},
|
||||
{
|
||||
"id": "8246f3fb-6d35-4a31-9bcd-ef81fd39c66f",
|
||||
"part": "Arpeggios Distorted",
|
||||
"instrument": {
|
||||
"id": "electric guitar",
|
||||
"description": "Electric Guitar",
|
||||
"created_at": "2013-01-03T01:57:43.040Z",
|
||||
"updated_at": "2013-01-03T01:57:43.040Z",
|
||||
"popularity": 3
|
||||
},
|
||||
"track_type": "Track",
|
||||
"position": 7,
|
||||
"preview_mp3_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/18%20And%20Life%20Stem%20-%20Electric%20Guitar%20-%20Arpeggios%20Distorted-44100-preview-2bf523d0f429eaff366691fff0dbcb5f.mp3",
|
||||
"preview_ogg_url": "https://jamkazam-public.s3.amazonaws.com/jam_track_previews/Skid%20Row/18%20And%20Life/18%20And%20Life%20Stem%20-%20Electric%20Guitar%20-%20Arpeggios%20Distorted-44100-preview-e25718a81d9d0a011746def84547f1e9.ogg",
|
||||
"preview_aac_url": null
|
||||
}
|
||||
],
|
||||
"licensor": { "id": "027d90a1-b126-4d5a-8af6-3167296dfb04", "name": "Tency Music" },
|
||||
"genres": ["Metal", "Rock", "Hard Rock"],
|
||||
"added_cart": false,
|
||||
"can_download": false,
|
||||
"purchased": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -20,4 +20,14 @@ import './commands'
|
|||
// require('./commands')
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
// Intercept the GET /app_features request and return an empty array
|
||||
// to simulate the backend returning an empty set of app features
|
||||
cy.intercept('GET', /\S+\/app_features/, {
|
||||
statusCode: 200,
|
||||
body: [],
|
||||
}).as('getAppFeatures');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3299,6 +3299,33 @@
|
|||
"requires": {
|
||||
"css-mediaquery": "^0.1.2",
|
||||
"prop-types": "^15.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@fingerprintjs/fingerprintjs": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-4.4.3.tgz",
|
||||
"integrity": "sha512-sm0ZmDp5Oeq8hQTf+bAHKsuuteVAYme/YOY9UPP/GrUBrR5Fzl1P5oOv6F5LvyBrO7qLjU5HQkfU0MmFte/8xA==",
|
||||
"requires": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
|
|
@ -3336,11 +3363,18 @@
|
|||
}
|
||||
},
|
||||
"@fortawesome/free-solid-svg-icons": {
|
||||
"version": "5.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz",
|
||||
"integrity": "sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q==",
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.35"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "0.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
|
||||
"integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@fortawesome/react-fontawesome": {
|
||||
|
|
@ -3349,6 +3383,18 @@
|
|||
"integrity": "sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@fullcalendar/bootstrap": {
|
||||
|
|
@ -3782,6 +3828,16 @@
|
|||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
|
||||
"integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -7224,6 +7280,11 @@
|
|||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"creditcard.js": {
|
||||
"version": "3.0.33",
|
||||
"resolved": "https://registry.npmjs.org/creditcard.js/-/creditcard.js-3.0.33.tgz",
|
||||
"integrity": "sha512-jECtlIZpmKsdCqvvYzD+lbmWq3ytNiwKrQq7+Cv4VuYNJH0yv1GqQacZ99Dp40cFY6SealIp0p94oKI8IbrnWQ=="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
|
|
@ -8259,6 +8320,18 @@
|
|||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"prop-types": "^15.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"emoji-regex": {
|
||||
|
|
@ -8956,6 +9029,17 @@
|
|||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "2.0.0-next.3",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz",
|
||||
|
|
@ -16448,13 +16532,13 @@
|
|||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.8.1"
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"prr": {
|
||||
|
|
@ -16670,6 +16754,18 @@
|
|||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-app-polyfill": {
|
||||
|
|
@ -16721,6 +16817,18 @@
|
|||
"requires": {
|
||||
"lodash": "^4.17.19",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-cookie": {
|
||||
|
|
@ -16741,6 +16849,18 @@
|
|||
"countup.js": "^1.9.3",
|
||||
"prop-types": "^15.7.2",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-datetime": {
|
||||
|
|
@ -16758,6 +16878,23 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
|
||||
"integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I="
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -16971,6 +17108,18 @@
|
|||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.19.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-dom-factories": {
|
||||
|
|
@ -16985,6 +17134,18 @@
|
|||
"requires": {
|
||||
"classnames": "^2.2.5",
|
||||
"prop-types": "^15.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-dropzone": {
|
||||
|
|
@ -16995,6 +17156,18 @@
|
|||
"attr-accept": "^2.0.0",
|
||||
"file-selector": "^0.1.12",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-error-overlay": {
|
||||
|
|
@ -17019,6 +17192,18 @@
|
|||
"@babel/runtime": "^7.2.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"warning": "^4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-flatpickr": {
|
||||
|
|
@ -17028,6 +17213,18 @@
|
|||
"requires": {
|
||||
"flatpickr": "^4.6.2",
|
||||
"prop-types": "^15.5.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-hook-form": {
|
||||
|
|
@ -17041,6 +17238,18 @@
|
|||
"integrity": "sha512-HWN/Ftgi31W2OAFVNo5BO1n8C9D2W0L+sque20dKuwh51hQuMwK4bMAtdrYAmapfGGh+53zQ1NsrSacVS1Q/mw==",
|
||||
"requires": {
|
||||
"prop-types": "^15.5.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-i18next": {
|
||||
|
|
@ -17069,6 +17278,18 @@
|
|||
"requires": {
|
||||
"prop-types": "^15.6.2",
|
||||
"react-modal": "^3.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-image-video-lightbox": {
|
||||
|
|
@ -17091,6 +17312,18 @@
|
|||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
|
|
@ -17102,6 +17335,18 @@
|
|||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17112,6 +17357,18 @@
|
|||
"integrity": "sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==",
|
||||
"requires": {
|
||||
"prop-types": "^15.5.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
|
|
@ -17135,6 +17392,16 @@
|
|||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
|
||||
"integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -17185,6 +17452,16 @@
|
|||
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-1.2.0.tgz",
|
||||
"integrity": "sha512-GHqzxLYImx1iKN1jJURcuRoA/0ygCcNhfGw1IT8nPIMzarmKQ3Nc+JcG0gi8JXQzuh0C5ShE4npMIoqNin40hg=="
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"react-simple-code-editor": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.10.0.tgz",
|
||||
|
|
@ -17210,6 +17487,18 @@
|
|||
"prop-types": "^15.5.10",
|
||||
"react-lifecycles-compat": "^3.0.0",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-onclickoutside": {
|
||||
|
|
@ -17224,6 +17513,18 @@
|
|||
"requires": {
|
||||
"popper.js": "^1.14.1",
|
||||
"prop-types": "^15.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-quill": {
|
||||
|
|
@ -17237,6 +17538,18 @@
|
|||
"prop-types": "^15.5.10",
|
||||
"quill": "^1.3.7",
|
||||
"react-dom-factories": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-rating": {
|
||||
|
|
@ -17271,6 +17584,18 @@
|
|||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.13.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-router": {
|
||||
|
|
@ -17288,6 +17613,18 @@
|
|||
"react-is": "^16.6.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-router-bootstrap": {
|
||||
|
|
@ -17296,6 +17633,18 @@
|
|||
"integrity": "sha512-/22eqxjn6Zv5fvY2rZHn57SKmjmJfK7xzJ6/G1OgxAjLtKVfWgV5sn41W2yiqzbtV5eE4/i4LeDLBGYTqx7jbA==",
|
||||
"requires": {
|
||||
"prop-types": "^15.5.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-router-dom": {
|
||||
|
|
@ -17310,6 +17659,18 @@
|
|||
"react-router": "5.2.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-scripts": {
|
||||
|
|
@ -17399,6 +17760,16 @@
|
|||
"xregexp": "^4.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
|
||||
|
|
@ -17419,6 +17790,18 @@
|
|||
"requires": {
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-scrollbars-custom": {
|
||||
|
|
@ -17455,6 +17838,16 @@
|
|||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
||||
|
|
@ -17485,6 +17878,26 @@
|
|||
"resize-observer-polyfill": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"react-slider": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/react-slider/-/react-slider-2.0.6.tgz",
|
||||
"integrity": "sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA==",
|
||||
"requires": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-timeago": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-6.2.1.tgz",
|
||||
|
|
@ -17510,6 +17923,16 @@
|
|||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
||||
|
|
@ -17532,6 +17955,18 @@
|
|||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-typed": {
|
||||
|
|
@ -17541,6 +17976,18 @@
|
|||
"requires": {
|
||||
"prop-types": "^15.6.0",
|
||||
"typed.js": "^2.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"reactstrap": {
|
||||
|
|
@ -17555,6 +18002,16 @@
|
|||
"react-transition-group": "^2.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"react-popper": {
|
||||
"version": "1.3.11",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.11.tgz",
|
||||
|
|
@ -19794,6 +20251,18 @@
|
|||
"is-function": "^1.0.1",
|
||||
"is-plain-object": "^2.0.1",
|
||||
"prop-types": "^15.5.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"throat": {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@farfetch/react-context-responsive": "^1.5.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.30",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.14.0",
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
"chance": "^1.1.8",
|
||||
"chart.js": "^2.9.3",
|
||||
"classnames": "^2.2.6",
|
||||
"creditcard.js": "^3.0.33",
|
||||
"custom-protocol-check": "^1.4.0",
|
||||
"echarts": "^4.9.0",
|
||||
"echarts-for-react": "^2.0.16",
|
||||
|
|
@ -76,6 +78,7 @@
|
|||
"react-select": "^3.1.0",
|
||||
"react-simple-code-editor": "^0.9.15",
|
||||
"react-slick": "^0.25.2",
|
||||
"react-slider": "^2.0.6",
|
||||
"react-timeago": "^6.2.1",
|
||||
"react-toastify": "^5.5.0",
|
||||
"react-typed": "^1.2.0",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -7,6 +7,10 @@
|
|||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.form-control-is-invalid{
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Choices */
|
||||
|
|
|
|||
|
|
@ -124,3 +124,66 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
//jamtrack slider
|
||||
.horizontal-slider {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
// height: 100vh;
|
||||
// margin: auto;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.jamtrack-thumb {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
background: #ffffff;
|
||||
border: 5px solid #3774ff;
|
||||
border-radius: 100%;
|
||||
display: block;
|
||||
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.40);
|
||||
}
|
||||
|
||||
.jamtrack-thumb.active {
|
||||
background-color: grey;
|
||||
}
|
||||
|
||||
.jamtrack-track {
|
||||
position: relative;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.jamtrack-track.jamtrack-track-0 {
|
||||
background: #83a9ff;
|
||||
}
|
||||
|
||||
.horizontal-slider .jamtrack-track {
|
||||
top: 10px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.horizontal-slider .jamtrack-thumb {
|
||||
top: 7px;
|
||||
width: 10px;
|
||||
outline: none;
|
||||
height: 10px;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
.num-circle {
|
||||
position: absolute;
|
||||
// top: "0px";
|
||||
// left: "10px";
|
||||
right: 0;
|
||||
background-color: #cc0e0e;
|
||||
color: #fff;
|
||||
font-family: tahoma, arial, 'sans-serif';
|
||||
font-size: 9px;
|
||||
width: 18px;
|
||||
line-height: 18px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
transform: translate(50%, -50%)
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { Card, CardBody } from 'reactstrap';
|
|||
import FalconCardHeader from '../common/FalconCardHeader';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import JKJamTracksAutoComplete from '../jamtracks/JKJamTracksAutoComplete';
|
||||
import { getJamTracks, getAffiliatePartnerData } from '../../helpers/rest';
|
||||
import { getJamTracks, getAffiliatePartnerData, autocompleteJamTracks } from '../../helpers/rest';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useResponsive } from '@farfetch/react-context-responsive';
|
||||
|
|
@ -118,6 +118,7 @@ const JKAffiliateLinks = () => {
|
|||
<p>{t('links.jamtracks_pages_paragraph')}</p>
|
||||
<div className='mt-4'>
|
||||
<JKJamTracksAutoComplete
|
||||
fetchFunc={autocompleteJamTracks}
|
||||
onSelect={handleOnSelect}
|
||||
onEnter={handleOnEnter}
|
||||
showDropdown={showDropdown}
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
const JKShoppingCart = () => {
|
||||
return (
|
||||
<div>ShoppingCart</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JKShoppingCart
|
||||
|
|
@ -23,7 +23,7 @@ const Avatar = ({ size, rounded, src, name, emoji, className, mediaClass, isExac
|
|||
</div>
|
||||
);
|
||||
} else {
|
||||
return <img className={mediaClasses} src={src} alt="" />;
|
||||
return <img className={mediaClasses.concat(classNames)} src={src} alt="" />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,8 +49,11 @@ import JKAffiliateEarnings from '../affiliate/JKAffiliateEarnings';
|
|||
import JKAffiliateAgreement from '../affiliate/JKAffiliateAgreement';
|
||||
|
||||
import JKJamTracksFilter from '../jamtracks/JKJamTracksFilter';
|
||||
|
||||
import JKShoppingCart from '../cart/JKShoppingCart';
|
||||
import JKShoppingCart from '../shopping-cart/JKShoppingCart';
|
||||
import JKCheckout from '../shopping-cart/JKCheckout';
|
||||
import JKCheckoutSuccess from '../shopping-cart/JKCheckoutSuccess';
|
||||
import JKMyJamTracks from '../jamtracks/JKMyJamTracks';
|
||||
import JKJamTrack from '../jamtracks/JKJamTrack';
|
||||
|
||||
|
||||
//import loadable from '@loadable/component';
|
||||
|
|
@ -265,10 +268,11 @@ function JKDashboardMain() {
|
|||
<div className="content">
|
||||
<NavbarTop />
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route path="/privacy" component={JKPrivacy} />
|
||||
<Route path="/help" component={JKHelp} />
|
||||
<Route path="/unsubscribe" exact component={JKUnsubscribe} />
|
||||
{/* <PrivateRoute path="/" exact component={HomePage} /> */}
|
||||
<PrivateRoute path="/" exact component={JKEditProfile} />
|
||||
<PrivateRoute path="/friends/my" component={JKMyFriends} />
|
||||
<PrivateRoute path="/friends" component={JKPeopleFilter} />
|
||||
<PrivateRoute path="/sessions/new" component={JKNewMusicSession} />
|
||||
|
|
@ -286,8 +290,12 @@ function JKDashboardMain() {
|
|||
<PrivateRoute path="/affiliate/signups" component={JKAffiliateSignups} />
|
||||
<PrivateRoute path="/affiliate/earnings" component={JKAffiliateEarnings} />
|
||||
<PrivateRoute path="/affiliate/agreement" component={JKAffiliateAgreement} />
|
||||
<PrivateRoute path="/jamtracks/:id" component={JKJamTrack} />
|
||||
<PrivateRoute path="/jamtracks" component={JKJamTracksFilter} />
|
||||
<PrivateRoute path="/cart" component={JKShoppingCart} />
|
||||
<PrivateRoute path="/my-jamtracks" component={JKMyJamTracks} />
|
||||
<PrivateRoute path="/shopping-cart" component={JKShoppingCart} />
|
||||
<PrivateRoute path="/checkout/success" component={JKCheckoutSuccess} />
|
||||
<PrivateRoute path="/checkout" component={JKCheckout} />
|
||||
{/*Redirect*/}
|
||||
<Redirect to="/errors/404" />
|
||||
</Switch>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Table, Row, Col, Input, Button } from 'reactstrap';
|
||||
import Select from 'react-select';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { createMyMixdown, addMixdown } from '../../store/features/jamTrackSlice';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Scrollbar } from 'react-scrollbars-custom';
|
||||
|
||||
const JKCreateCustomMix = () => {
|
||||
const MAX_MIXDOWNS = 5;
|
||||
|
||||
const [tracks, setTracks] = useState([]);
|
||||
const [mixdowns, setMixdowns] = useState([]);
|
||||
const [selectedTracks, setSelectedTracks] = useState([]);
|
||||
const dispatch = useDispatch();
|
||||
const scrollbar = useRef();
|
||||
|
||||
const TEMPO_OPTIONS = [
|
||||
{ value: '0', label: 'Original tempo' },
|
||||
{ value: '-5', label: 'Slower by 5%' },
|
||||
{ value: '-10', label: 'Slower by 10%' },
|
||||
{ value: '-15', label: 'Slower by 15%' },
|
||||
{ value: '-20', label: 'Slower by 20%' },
|
||||
{ value: '-25', label: 'Slower by 25%' },
|
||||
{ value: '-30', label: 'Slower by 30%' },
|
||||
{ value: '-35', label: 'Slower by 35%' },
|
||||
{ value: '-40', label: 'Slower by 40%' },
|
||||
{ value: '-45', label: 'Slower by 45%' },
|
||||
{ value: '-50', label: 'Slower by 50%' },
|
||||
{ value: '-60', label: 'Slower by 60%' },
|
||||
{ value: '-70', label: 'Slower by 70%' },
|
||||
{ value: '-80', label: 'Slower by 80%' },
|
||||
{ value: '5', label: 'Faster by 5%' },
|
||||
{ value: '10', label: 'Faster by 10%' },
|
||||
{ value: '15', label: 'Faster by 15%' },
|
||||
{ value: '20', label: 'Faster by 20%' },
|
||||
{ value: '30', label: 'Faster by 30%' },
|
||||
{ value: '40', label: 'Faster by 40%' },
|
||||
{ value: '50', label: 'Faster by 50%' }
|
||||
];
|
||||
|
||||
const PITCH_OPTIONS = [
|
||||
{ value: '0', label: 'Original pitch' },
|
||||
{ value: '-1', label: 'Down 1 semitone' },
|
||||
{ value: '-2', label: 'Down 2 semitone' },
|
||||
{ value: '-3', label: 'Down 3 semitone' },
|
||||
{ value: '-4', label: 'Down 4 semitone' },
|
||||
{ value: '-5', label: 'Down 5 semitone' },
|
||||
{ value: '-6', label: 'Down 6 semitone' },
|
||||
{ value: '-7', label: 'Down 7 semitone' },
|
||||
{ value: '-8', label: 'Down 8 semitone' },
|
||||
{ value: '-9', label: 'Down 9 semitone' },
|
||||
{ value: '-10', label: 'Down 10 semitone' },
|
||||
{ value: '-11', label: 'Down 11 semitone' },
|
||||
{ value: '-12', label: 'Down 12 semitone' },
|
||||
{ value: '1', label: 'Up 1 semitone' },
|
||||
{ value: '2', label: 'Up 2 semitone' },
|
||||
{ value: '3', label: 'Up 3 semitone' },
|
||||
{ value: '4', label: 'Up 4 semitone' },
|
||||
{ value: '5', label: 'Up 5 semitone' },
|
||||
{ value: '6', label: 'Up 6 semitone' },
|
||||
{ value: '7', label: 'Up 7 semitone' },
|
||||
{ value: '8', label: 'Up 8 semitone' },
|
||||
{ value: '9', label: 'Up 9 semitone' },
|
||||
{ value: '10', label: 'Up 10 semitone' },
|
||||
{ value: '11', label: 'Up 11 semitone' },
|
||||
{ value: '12', label: 'Up 12 semitone' }
|
||||
];
|
||||
|
||||
const jamTrack = useSelector(state => state.jamTrack.jamTrack);
|
||||
const newMixdownLoadingStatus = useSelector(state => state.jamTrack.newMixdownLoadingStatus);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
getValues
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
mixName: '',
|
||||
tempo: {
|
||||
value: '0',
|
||||
label: 'Original tempo'
|
||||
},
|
||||
pitch: {
|
||||
value: '0',
|
||||
label: 'Original pitch'
|
||||
},
|
||||
mixdownTracks: []
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = data => {
|
||||
const _tracks = [];
|
||||
let countIn = false;
|
||||
const selected = getValues('mixdownTracks');
|
||||
tracks.forEach(track => {
|
||||
const muted = selected.includes(track.id);
|
||||
if (track.id === 'count-in') {
|
||||
if (countIn === false) {
|
||||
countIn = !muted;
|
||||
}
|
||||
} else {
|
||||
_tracks.push({
|
||||
id: track.id,
|
||||
mute: selected.includes(track.id)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setValue('mixdownTracks', _tracks);
|
||||
|
||||
const mixData = {
|
||||
jamTrackID: jamTrack.id,
|
||||
name: data.mixName,
|
||||
settings: { speed: parseInt(data.tempo.value), pitch: parseInt(data.pitch.value), 'count-in': countIn, tracks: _tracks }
|
||||
};
|
||||
|
||||
const tempMixdown = {...mixData, id: 'temp', jam_track_id: jamTrack.id};
|
||||
dispatch(addMixdown(tempMixdown));
|
||||
|
||||
dispatch(createMyMixdown(mixData));
|
||||
};
|
||||
|
||||
const toggleTrack = e => {
|
||||
const trackId = e.target.value;
|
||||
const selected = getValues('mixdownTracks');
|
||||
if (selected.includes(trackId)) {
|
||||
setValue('mixdownTracks', selectedTracks.filter(track => track !== trackId));
|
||||
} else {
|
||||
setValue('mixdownTracks', [...selectedTracks, trackId]);
|
||||
}
|
||||
setSelectedTracks(getValues('mixdownTracks'));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (jamTrack) {
|
||||
setTracks(jamTrack.tracks.filter(track => track.track_type === 'Track' || track.track_type === 'Click'));
|
||||
setMixdowns(jamTrack.mixdowns);
|
||||
}
|
||||
}, [jamTrack]);
|
||||
|
||||
useEffect(() => {
|
||||
if (jamTrack) {
|
||||
if(newMixdownLoadingStatus === 'succeeded') {
|
||||
setValue('mixName', '');
|
||||
setValue('tempo', TEMPO_OPTIONS[0]);
|
||||
setValue('pitch', PITCH_OPTIONS[0]);
|
||||
setValue('mixdownTracks', []);
|
||||
setSelectedTracks([]);
|
||||
setMixdowns(jamTrack.mixdowns);
|
||||
}
|
||||
}
|
||||
}, [newMixdownLoadingStatus]);
|
||||
|
||||
const trackName = track => {
|
||||
if (track.track_type === 'Track' || track.track_type === 'Click') {
|
||||
if (track.track_type === 'Click') {
|
||||
return 'Clicktrack';
|
||||
} else if (track.instrument) {
|
||||
const instrumentDescription = track.instrument.description;
|
||||
let part = '';
|
||||
if (track.part && track.part !== instrumentDescription) {
|
||||
part = `(${track.part})`;
|
||||
}
|
||||
return `${instrumentDescription} ${part}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const hasExceededMax = mixdowns.length >= MAX_MIXDOWNS;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
Mute any tracks you like. Adjust the pitch or tempo of playback. Then give your mix a descriptive name, and
|
||||
click the Create Mix button. It will take few minutes for us to create your custom mix.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Row>
|
||||
<Col>
|
||||
<Scrollbar ref={scrollbar} style={{ width: '100%', height: 300 }} mobileNative={true}>
|
||||
<Table striped bordered className="fs--1 mb-0">
|
||||
<thead className="bg-200 text-900">
|
||||
<tr>
|
||||
<th>Tracks {tracks.length > 0 && <>({tracks.length})</>}</th>
|
||||
<th class="text-center">Mute</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{tracks &&
|
||||
tracks.map((track, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<span>{trackName(track)}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" value={track.id} onClick={toggleTrack} checked={ selectedTracks.includes(track.id)} disabled={hasExceededMax} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
</tbody>
|
||||
</Table>
|
||||
</Scrollbar>
|
||||
<Controller
|
||||
name="mixdownTracks"
|
||||
control={control}
|
||||
rules={{
|
||||
required: 'Select at least one track to create a mix'
|
||||
}}
|
||||
render={({ field }) => <Input type='hidden' {...field} />}
|
||||
/>
|
||||
{errors.mixdownTracks && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.mixdownTracks.message}</small>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="mb-3 mt-3">
|
||||
<Col sm={6} md={4} lg={3}>Tempo</Col>
|
||||
<Col>
|
||||
<Controller
|
||||
name="tempo"
|
||||
control={control}
|
||||
render={({ field }) => <Select {...field} options={TEMPO_OPTIONS} isDisabled={hasExceededMax} />}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mb-3">
|
||||
<Col sm={6} md={4} lg={3}>Pitch</Col>
|
||||
<Col>
|
||||
<Controller
|
||||
name="pitch"
|
||||
control={control}
|
||||
render={({ field }) => <Select {...field} options={PITCH_OPTIONS} isDisabled={hasExceededMax} />}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mb-3">
|
||||
<Col sm={6} md={4} lg={3}>Mix Name</Col>
|
||||
<Col>
|
||||
<Controller
|
||||
name="mixName"
|
||||
control={control}
|
||||
rules={{
|
||||
required: 'Mix name is required'
|
||||
}}
|
||||
render={({ field }) => <Input {...field} disabled={hasExceededMax} />}
|
||||
/>
|
||||
{errors.mixName && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.mixName.message}</small>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col className='d-flex justify-content-end'>
|
||||
<Button color="primary" disabled={newMixdownLoadingStatus === 'loading' || hasExceededMax }>
|
||||
{newMixdownLoadingStatus === 'loading' ? 'Creating Mix...' : 'Create Mix'}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKCreateCustomMix;
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useResponsive } from '@farfetch/react-context-responsive';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Card, CardBody, Row, Col } from 'reactstrap';
|
||||
import FalconCardHeader from '../common/FalconCardHeader';
|
||||
import { getUserDetail, postUserEvent, userOpenedJamTrackWebPlayer } from '../../helpers/rest';
|
||||
import JKJamTrackPlayer from './JKJamTrackPlayer';
|
||||
import JKMyJamTrackMixes from './JKMyJamTrackMixes';
|
||||
import JKCreateCustomMix from './JKCreateCustomMix';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
|
||||
import { fetchJamTrack } from '../../store/features/jamTrackSlice';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
const JKJamTrack = () => {
|
||||
console.log('JKJamTrack rendering');
|
||||
|
||||
const { t } = useTranslation('jamtracks');
|
||||
const { greaterThan } = useResponsive();
|
||||
const { id } = useParams();
|
||||
|
||||
const { currentUser } = useAuth();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const jamTrack = useSelector(state => state.jamTrack.jamTrack);
|
||||
const jamTrackLoadingStatus = useSelector(state => state.jamTrack.status);
|
||||
|
||||
const fetchJamTrackRecord = () => {
|
||||
dispatch(fetchJamTrack({ id }));
|
||||
};
|
||||
|
||||
const fetchUserDetail = async () => {
|
||||
try {
|
||||
const userId = currentUser.id;
|
||||
const resp = await getUserDetail({ id: userId });
|
||||
const data = await resp.json();
|
||||
console.log('user detail', data);
|
||||
await postUserEvent({ name: 'jamtrack_web_player_open' });
|
||||
if (!data.first_opened_jamtrack_web_player) {
|
||||
setTimeout(async () => {
|
||||
await userOpenedJamTrackWebPlayer();
|
||||
}, 15000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error when fetching user detail', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser && jamTrack) {
|
||||
fetchUserDetail();
|
||||
}
|
||||
}, [currentUser, jamTrack]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchJamTrackRecord();
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{jamTrackLoadingStatus === 'loading' || jamTrackLoadingStatus == 'idel' ? (
|
||||
<div>Loading...</div>
|
||||
) : Object.keys(jamTrack).length ? (
|
||||
<Row>
|
||||
<Col sm={12} md={4}>
|
||||
<Card className="mx-auto mb-4">
|
||||
<FalconCardHeader title={t('jamtrack.player.title')} titleClass="font-weight-semi-bold" />
|
||||
<CardBody className="pt-3">{jamTrack && <JKJamTrackPlayer />}</CardBody>
|
||||
</Card>
|
||||
<Card className="mx-auto">
|
||||
<FalconCardHeader title={t('jamtrack.my_mixes.title')} titleClass="font-weight-semi-bold" />
|
||||
<CardBody className="pt-3">{jamTrack && <JKMyJamTrackMixes />}</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col sm={12} md={4} className={ greaterThan.sm ? null : 'mt-4' }>
|
||||
<Card className="mx-auto">
|
||||
<FalconCardHeader title={t('jamtrack.create_mix.title')} titleClass="font-weight-semi-bold" />
|
||||
<CardBody className="pt-3">{jamTrack && <JKCreateCustomMix />}</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col />
|
||||
</Row>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKJamTrack;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Row, Col } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const JKJamTrackArtists = ({ artists, showArtists, onSelect }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
|
@ -31,7 +32,7 @@ const JKJamTrackArtists = ({ artists, showArtists, onSelect }) => {
|
|||
onClick={() => handleClick(artist)}
|
||||
className={index + 1 > 6 && !expanded ? 'd-none' : null}
|
||||
>
|
||||
<span className='mr-2 pb-1'>
|
||||
<span className='mr-4 pb-1'>
|
||||
{artist.original_artist}
|
||||
</span>
|
||||
</a>
|
||||
|
|
@ -58,4 +59,16 @@ const JKJamTrackArtists = ({ artists, showArtists, onSelect }) => {
|
|||
);
|
||||
};
|
||||
|
||||
JKJamTrackArtists.propTypes = {
|
||||
artists: PropTypes.array.isRequired,
|
||||
showArtists: PropTypes.bool,
|
||||
onSelect: PropTypes.func
|
||||
};
|
||||
|
||||
JKJamTrackArtists.defaultProps = {
|
||||
artists: [],
|
||||
showArtists: false,
|
||||
onSelect: () => {}
|
||||
};
|
||||
|
||||
export default JKJamTrackArtists;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react'
|
||||
|
||||
const JKJamTrackMyMixes = () => {
|
||||
return (
|
||||
<div>JKJamTrackMyMixes</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JKJamTrackMyMixes
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import Select from 'react-select';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Row, Col } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { markMixdownActive } from '../../helpers/rest';
|
||||
import useJamTrackAudio from '../../hooks/useJamTrackAudio';
|
||||
|
||||
const JKJamTrackPlayer = ({ jamTrack }) => {
|
||||
const [mixes, setMixes] = useState([]);
|
||||
const [options, setOptions] = useState([]);
|
||||
const [selectedMix, setSelectedMix] = useState(null);
|
||||
const { audioUrls, loadJamTrack } = useJamTrackAudio(jamTrack);
|
||||
|
||||
const handleChange = selectedOption => {
|
||||
const mix = mixes.find(mix => mix.value === selectedOption.value);
|
||||
setSelectedMix(mix);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (jamTrack) {
|
||||
console.log('_JamTrack_ jamTrack', jamTrack);
|
||||
const _opts = jamTrack.mixdowns.map(mix => ({ value: mix.id, label: mix.name }));
|
||||
_opts.unshift({ value: 'original', label: 'Original' });
|
||||
setOptions(_opts);
|
||||
|
||||
//set the default mix to the original
|
||||
const activeMix = jamTrack.mixdowns.find(mix => mix.id === jamTrack.last_mixdown_id)
|
||||
|
||||
console.log('_JamTrack_ activeMix', activeMix);
|
||||
|
||||
setSelectedMix(activeMix);
|
||||
}
|
||||
}, [jamTrack]);
|
||||
|
||||
const activateMasterTrack = async () => {
|
||||
console.log('playing original');
|
||||
await markMixdownActive({ id: jamTrack.id, mixdown_id: null });
|
||||
await loadJamTrack();
|
||||
};
|
||||
|
||||
const activateCustomMix = async () => {
|
||||
console.log('playing mix', selectedMix.value);
|
||||
try {
|
||||
await markMixdownActive({ id: jamTrack.id, mixdown_id: selectedMix.value });
|
||||
await loadJamTrack();
|
||||
}catch(error){
|
||||
console.log('Error when activating custom mix', error);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedMix) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('_JamTrack_ selectedMix', selectedMix);
|
||||
|
||||
if (selectedMix.value === 'original') {
|
||||
//console.log('_JAMTRACK_ activating master track');
|
||||
activateMasterTrack().then(() => {
|
||||
//TODO: commiunicate with the client back end. Following is copied from the Rails front end
|
||||
//SessionActions.mixdownActive({id:null})
|
||||
});
|
||||
} else {
|
||||
//console.log('_JAMTRACK_ activating custom mix:', selectedMix);
|
||||
activateCustomMix().then(() => {
|
||||
//TODO: commiunicate with the client back end. Following is copied from the Rails front end
|
||||
//context.jamClient.JamTrackStopPlay();
|
||||
//SessionActions.mixdownActive(mixdown)
|
||||
});
|
||||
}
|
||||
}, [selectedMix]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select options={mixes} placeholder="Select Mix" onChange={handleChange} value={selectedMix} />
|
||||
{ JSON.stringify(audioUrls)}
|
||||
<Row className='mt-2'>
|
||||
<Col>
|
||||
{audioUrls.length > 0 && (
|
||||
<figure>
|
||||
<audio controls style={{ width: '100%'}}>
|
||||
{audioUrls.map((url, index) => (
|
||||
<source key={index} src={url} type={`audio/${url.split('.').pop()}`} />
|
||||
))}
|
||||
</audio>
|
||||
</figure>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
JKJamTrackPlayer.propTypes = {
|
||||
jamTrack: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default JKJamTrackPlayer;
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import Select from 'react-select';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Row, Col, Progress } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { markMixdownActive } from '../../helpers/rest';
|
||||
import useBrowserMedia from '../../hooks/useBrowserMedia';
|
||||
import JKProgressSlider from './JKProgressSlider';
|
||||
|
||||
const JKJamTrackPlayer = ({ jamTrack }) => {
|
||||
const [mixes, setMixes] = useState([]);
|
||||
const [selectedMix, setSelectedMix] = useState(null);
|
||||
const { play, stop, pause, loading, loaded, playing, paused, loadError, playPosition } = useBrowserMedia(jamTrack);
|
||||
|
||||
const handleChange = selectedOption => {
|
||||
//console.log('selectedOption', selectedOption);
|
||||
setSelectedMix(selectedOption);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (jamTrack) {
|
||||
const mixes = jamTrack.mixdowns.map(mix => ({ value: mix.id, label: mix.name, mix }));
|
||||
mixes.unshift({ value: 'original', label: 'Original', jamTrack });
|
||||
setMixes(mixes);
|
||||
}
|
||||
}, [jamTrack]);
|
||||
|
||||
const trackDuration = useMemo(() => {
|
||||
if (jamTrack && jamTrack.duration) {
|
||||
return (jamTrack.duration/60).toFixed(2);
|
||||
}
|
||||
}, [jamTrack]);
|
||||
|
||||
const trackCurrentTime = useMemo(() => {
|
||||
if (playPosition) {
|
||||
return (playPosition/60).toFixed(2);
|
||||
}else{
|
||||
return '0.00';
|
||||
}
|
||||
}, [playPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!selectedMix) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePlayOriginal = async () => {
|
||||
console.log('playing original');
|
||||
await markMixdownActive({id: selectedMix.jamTrack.id, mixdown_id: null});
|
||||
}
|
||||
|
||||
const handlePlayMix = async () => {
|
||||
console.log('playing mix', selectedMix.value);
|
||||
await markMixdownActive({id: selectedMix.jamTrack.id, mixdown_id: selectedMix.value});
|
||||
}
|
||||
|
||||
|
||||
if(selectedMix.value === 'original') {
|
||||
console.log('playing original');
|
||||
handlePlayOriginal();
|
||||
} else {
|
||||
console.log('playing mix', selectedMix.value);
|
||||
handlePlayMix();
|
||||
}
|
||||
}, [selectedMix]);
|
||||
|
||||
const playAudio = () => {
|
||||
console.log('playing');
|
||||
play();
|
||||
}
|
||||
|
||||
const stopAudio = () => {
|
||||
console.log('stopping');
|
||||
stop();
|
||||
}
|
||||
|
||||
const pauseAudio = () => {
|
||||
console.log('pausing');
|
||||
pause();
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select options={mixes} placeholder="Select Mix" onChange={handleChange} />
|
||||
{/* <Row className="mt-2 align-items-center">
|
||||
<Col className="col-md-2">
|
||||
<div className="d-flex">
|
||||
{ playing && <FontAwesomeIcon icon="pause-circle" size="2x" onClick={pauseAudio} /> }
|
||||
{ !playing && <FontAwesomeIcon icon="play-circle" size="2x" onClick={playAudio} /> }
|
||||
<FontAwesomeIcon icon="stop-circle" size="2x" onClick={stopAudio} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div className='d-flex'>
|
||||
<span>{trackCurrentTime}</span>
|
||||
<JKProgressSlider />
|
||||
<span>{trackDuration}</span>
|
||||
</div>
|
||||
|
||||
</Col>
|
||||
</Row> */}
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
JKJamTrackPlayer.propTypes = {
|
||||
jamTrack: PropTypes.object.isRequired,
|
||||
|
||||
};
|
||||
|
||||
export default JKJamTrackPlayer;
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import Select from 'react-select';
|
||||
import { Row, Col } from 'reactstrap';
|
||||
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
const JKJamTrackPlayer = () => {
|
||||
const [options, setOptions] = useState([]);
|
||||
const [selectedOption, setSelectedOption] = useState(null);
|
||||
const fpPromise = FingerprintJS.load();
|
||||
const [audioUrl, setAudioUrl] = useState(null);
|
||||
const audioRef = useRef(null);
|
||||
const jamTrack = useSelector(state => state.jamTrack.jamTrack);
|
||||
|
||||
useEffect(() => {
|
||||
if (jamTrack) {
|
||||
const opts = jamTrack.mixdowns.map(mix => ({ value: mix.id, label: mix.name })).filter(mix => mix.value !== 'temp');
|
||||
opts.unshift({ value: 'original', label: 'Original' });
|
||||
setOptions(opts);
|
||||
if (jamTrack.last_mixdown_id) {
|
||||
setSelectedOption(opts.find(opt => opt.value === jamTrack.last_mixdown_id));
|
||||
} else {
|
||||
setSelectedOption(opts[0]);
|
||||
}
|
||||
}
|
||||
}, [jamTrack]);
|
||||
|
||||
const handleOnChange = selectedOption => {
|
||||
const option = options.find(opt => opt.value === selectedOption.value);
|
||||
setSelectedOption(option);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedOption.value === 'original') {
|
||||
const audioUrl = getMasterTrack();
|
||||
setAudioUrl(audioUrl);
|
||||
if(audioRef.current)
|
||||
audioRef.current.load();
|
||||
} else {
|
||||
//it's a mixdown
|
||||
getMixdown().then(audioUrl => {
|
||||
setAudioUrl(audioUrl);
|
||||
if(audioRef.current)
|
||||
audioRef.current.load();
|
||||
});
|
||||
}
|
||||
}, [selectedOption]);
|
||||
|
||||
const getMasterTrack = () => {
|
||||
const masterTrack = jamTrack.tracks.find(track => track.track_type === 'Master');
|
||||
if (masterTrack) {
|
||||
const audioUrl = masterTrack.preview_mp3_url;
|
||||
return audioUrl;
|
||||
}
|
||||
};
|
||||
|
||||
const getMixdown = async () => {
|
||||
const activeMix = jamTrack.mixdowns.find(mix => mix.id === selectedOption.value);
|
||||
const fp = await fpPromise;
|
||||
const result = await fp.get();
|
||||
const audioUrl =
|
||||
process.env.REACT_APP_API_BASE_URL +
|
||||
`/mixdowns/${activeMix.id}/download.mp3?file_type=mp3&sample_rate=48&mark=${result.visitorId}`;
|
||||
return audioUrl;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select options={options} placeholder="Select Mix" onChange={handleOnChange} value={selectedOption} />
|
||||
<Row className="mt-2">
|
||||
<Col>
|
||||
{audioUrl && (
|
||||
<figure>
|
||||
<audio controls style={{ width: '100%' }} ref={audioRef}>
|
||||
<source src={audioUrl} type={`audio/${audioUrl.split('.').pop()}`} />
|
||||
</audio>
|
||||
</figure>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKJamTrackPlayer;
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { Fragment, useState } from 'react';
|
||||
import { Row, Col, Container } from 'reactstrap';
|
||||
import JKJamTrackTrack from './JKJamTrackTrack';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const JKJamTrackPreview = ({ jamTrack }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
|
@ -34,4 +35,8 @@ const JKJamTrackPreview = ({ jamTrack }) => {
|
|||
);
|
||||
};
|
||||
|
||||
JKJamTrackPreview.propTypes = {
|
||||
jamTrack: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default JKJamTrackPreview;
|
||||
|
|
|
|||
|
|
@ -4,25 +4,26 @@ import PropTypes from 'prop-types';
|
|||
import { addJamtrackToShoppingCart } from '../../helpers/rest';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useShoppingCart } from '../../hooks/useShoppingCart';
|
||||
|
||||
const JKJamTrackPurchaseButton = ({ jamTrack }) => {
|
||||
const history = useHistory();
|
||||
const { currentUser } = useAuth();
|
||||
console.log('currentUser', currentUser);
|
||||
const addToCart = () => {
|
||||
console.log('Add to Cart');
|
||||
const { addCartItem } = useShoppingCart();
|
||||
|
||||
const addToCart = async () => {
|
||||
const options = {
|
||||
id: jamTrack.id,
|
||||
variant: 'full'
|
||||
};
|
||||
addJamtrackToShoppingCart(options)
|
||||
.then(response => {
|
||||
console.log('Add to Cart Response', response);
|
||||
history.push('/cart');
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Add to Cart Error', error);
|
||||
});
|
||||
if (await addCartItem(options)) {
|
||||
toast.success('JamTrack added to cart');
|
||||
history.push('/shopping-cart');
|
||||
} else {
|
||||
console.log('Add to Cart Error');
|
||||
toast.error('Error adding to cart');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -53,4 +54,8 @@ const JKJamTrackPurchaseButton = ({ jamTrack }) => {
|
|||
);
|
||||
};
|
||||
|
||||
JKJamTrackPurchaseButton.propTypes = {
|
||||
jamTrack: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default JKJamTrackPurchaseButton;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import JKInstrumentIcon from '../profile/JKInstrumentIcon';
|
|||
import { Howl } from 'howler';
|
||||
import { useJamTrackPreview } from '../../context/JamTrackPreviewContext';
|
||||
import { Spinner } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const JKJamTrackTrack = ({ track }) => {
|
||||
console.log('debug JKTrackPlayPause track');
|
||||
|
|
@ -129,4 +130,8 @@ const JKJamTrackTrack = ({ track }) => {
|
|||
);
|
||||
};
|
||||
|
||||
JKJamTrackTrack.propTypes = {
|
||||
track: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default JKJamTrackTrack;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,20 @@ import { Row, Col, FormGroup, Input, InputGroup, InputGroupText, ListGroup, List
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { autocompleteJamTracks } from '../../helpers/rest';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const JKJamTracksAutoComplete = ({ onSelect, onEnter, showDropdown, setShowDropdown, inputValue, setInputValue, inputPlaceholder }) => {
|
||||
const JKJamTracksAutoComplete = ({
|
||||
fetchFunc,
|
||||
onSelect,
|
||||
onEnter,
|
||||
showDropdown,
|
||||
setShowDropdown,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
inputPlaceholder
|
||||
}) => {
|
||||
const [artists, setArtists] = useState([]);
|
||||
const [songs, setSongs] = useState([]);
|
||||
//const [showDropdown, setShowDropdown] = useState(false);
|
||||
//const [inputValue, setInputValue] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -18,22 +26,33 @@ const JKJamTracksAutoComplete = ({ onSelect, onEnter, showDropdown, setShowDropd
|
|||
const fetchAutoCompleteResults = useCallback(() => {
|
||||
// fetch tracks
|
||||
setLoading(true);
|
||||
autocompleteJamTracks(inputValue, MIN_FETCH_LIMIT)
|
||||
fetchFunc(inputValue, MIN_FETCH_LIMIT)
|
||||
.then(resp => {
|
||||
return resp.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('tracks', data);
|
||||
const updatedSongs = data.songs.map(song => {
|
||||
song.type = 'song';
|
||||
return song;
|
||||
});
|
||||
setSongs(updatedSongs);
|
||||
const updatedArtists = data.artists.map(artist => {
|
||||
artist.type = 'artist';
|
||||
return artist;
|
||||
});
|
||||
setArtists(updatedArtists);
|
||||
if (data.songs) {
|
||||
const updatedSongs = data.songs.map(song => {
|
||||
song.type = 'song';
|
||||
return song;
|
||||
});
|
||||
setSongs(updatedSongs);
|
||||
}
|
||||
if (data.artists) {
|
||||
const updatedArtists = data.artists.map(artist => {
|
||||
artist.type = 'artist';
|
||||
return artist;
|
||||
});
|
||||
setArtists(updatedArtists);
|
||||
}
|
||||
if(data.jamtracks){
|
||||
const updatedSongs = data.jamtracks.map(song => {
|
||||
song.type = 'song';
|
||||
return song;
|
||||
});
|
||||
setSongs(updatedSongs);
|
||||
}
|
||||
setShowDropdown(true);
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -151,4 +170,25 @@ const JKJamTracksAutoComplete = ({ onSelect, onEnter, showDropdown, setShowDropd
|
|||
);
|
||||
};
|
||||
|
||||
JKJamTracksAutoComplete.propTypes = {
|
||||
fetchFunc: PropTypes.func.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onEnter: PropTypes.func.isRequired,
|
||||
showDropdown: PropTypes.bool.isRequired,
|
||||
setShowDropdown: PropTypes.func.isRequired,
|
||||
inputValue: PropTypes.string.isRequired,
|
||||
setInputValue: PropTypes.func.isRequired,
|
||||
inputPlaceholder: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
JKJamTracksAutoComplete.defaultProps = {
|
||||
onSelect: () => {},
|
||||
onEnter: () => {},
|
||||
showDropdown: false,
|
||||
setShowDropdown: () => {},
|
||||
inputValue: '',
|
||||
setInputValue: () => {},
|
||||
inputPlaceholder: ''
|
||||
};
|
||||
|
||||
export default React.memo(JKJamTracksAutoComplete);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Card, CardBody, Row, Col } from 'reactstrap';
|
|||
import FalconCardHeader from '../common/FalconCardHeader';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import JKJamTracksAutoComplete from './JKJamTracksAutoComplete';
|
||||
import { getJamTracks, getJamTrackArtists } from '../../helpers/rest';
|
||||
import { getJamTracks, getJamTrackArtists, autocompleteJamTracks } from '../../helpers/rest';
|
||||
import JKJamTrackArtists from './JKJamTrackArtists';
|
||||
import JKJamTracksList from './JKJamTracksList';
|
||||
|
||||
|
|
@ -15,8 +15,10 @@ const JKJamTracksFilter = () => {
|
|||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [showArtists, setShowArtists] = useState(false);
|
||||
const [jamTracksNextPage, setJamTracksNextPage] = useState(null);
|
||||
const [nextOffset, setNextOffset] = useState(null);
|
||||
const [autoCompleteInputValue, setAutoCompleteInputValue] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const PER_PAGE = 10;
|
||||
|
||||
useEffect(() => {
|
||||
if (selected) {
|
||||
|
|
@ -24,20 +26,10 @@ const JKJamTracksFilter = () => {
|
|||
}
|
||||
}, [selected]);
|
||||
|
||||
const handleOnSelect = selected => {
|
||||
setArtists([]);
|
||||
setJamTracks([]);
|
||||
setSearchTerm('');
|
||||
setShowArtists(false);
|
||||
setSelected(selected);
|
||||
const params = queryOptions(selected);
|
||||
console.log('handleOnSelect _params_', params);
|
||||
fetchJamTracks(params);
|
||||
};
|
||||
|
||||
const queryOptions = selected => {
|
||||
const options = {
|
||||
limit: 100
|
||||
per_page: PER_PAGE,
|
||||
page: page
|
||||
};
|
||||
|
||||
if (typeof selected === 'string') {
|
||||
|
|
@ -51,47 +43,80 @@ const JKJamTracksFilter = () => {
|
|||
options.song = selected.name;
|
||||
}
|
||||
|
||||
if (jamTracksNextPage !== null) {
|
||||
options.next = jamTracksNextPage;
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
const handleOnEnter = queryStr => {
|
||||
const handleOnSelect = async (selected) => {
|
||||
setPage(1);
|
||||
setArtists([]);
|
||||
setJamTracks([]);
|
||||
setSelected(null);
|
||||
setSearchTerm('');
|
||||
setShowArtists(false);
|
||||
setSelected(selected);
|
||||
const params = queryOptions(selected);
|
||||
await fetchJamTracks(params);
|
||||
};
|
||||
|
||||
const handleOnEnter = async(queryStr) => {
|
||||
setPage(1);
|
||||
setArtists([]);
|
||||
setJamTracks([]);
|
||||
setSelected(x => null);
|
||||
setSearchTerm(queryStr);
|
||||
fetchArtists(queryStr);
|
||||
const params = queryOptions(queryStr);
|
||||
console.log('handleOnEnter _params_', params);
|
||||
fetchJamTracks(params);
|
||||
console.log('handleOnEnter _params', params, selected);
|
||||
await fetchJamTracks(params);
|
||||
};
|
||||
|
||||
const handleOnSelectArtist = artist => {
|
||||
const handleOnSelectArtist = async(artist) => {
|
||||
setPage(1);
|
||||
const selectedOpt = {
|
||||
type: 'artist',
|
||||
original_artist: artist.original_artist
|
||||
};
|
||||
setShowDropdown(false);
|
||||
setAutoCompleteInputValue('');
|
||||
handleOnSelect(selectedOpt);
|
||||
await handleOnSelect(selectedOpt);
|
||||
};
|
||||
|
||||
const fetchJamTracks = options => {
|
||||
getJamTracks(options)
|
||||
.then(resp => {
|
||||
return resp.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('jamtracks', data);
|
||||
setJamTracks(data.jamtracks);
|
||||
setJamTracksNextPage(data.next);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('error', error);
|
||||
});
|
||||
};
|
||||
const handleOnNextJamTracksPage = async () => {
|
||||
const currentQuery = selected ? selected : searchTerm;
|
||||
const params = queryOptions(currentQuery);
|
||||
await fetchJamTracks(params);
|
||||
}
|
||||
|
||||
// const fetchJamTracks = options => {
|
||||
// getJamTracks(options)
|
||||
// .then(resp => {
|
||||
// return resp.json();
|
||||
// })
|
||||
// .then(data => {
|
||||
// console.log('jamtracks', data);
|
||||
// setJamTracks(prev => [...prev, ...data.jamtracks]);
|
||||
// setNextOffset(data.next);
|
||||
// setPage(page => page + 1);
|
||||
// })
|
||||
// .catch(error => {
|
||||
// console.error('error', error);
|
||||
// });
|
||||
// };
|
||||
|
||||
|
||||
const fetchJamTracks = async(options) => {
|
||||
try {
|
||||
console.log('fetchJamTracks', options);
|
||||
const resp = await getJamTracks(options);
|
||||
const data = await resp.json();
|
||||
console.log('jamtracks', data);
|
||||
setJamTracks(prev => [...prev, ...data.jamtracks]);
|
||||
setNextOffset(data.next);
|
||||
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const fetchArtists = query => {
|
||||
const options = {
|
||||
|
|
@ -113,12 +138,6 @@ const JKJamTracksFilter = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleOnNextJamTracksPage = () => {
|
||||
const currentQuery = selected ? selected : searchTerm;
|
||||
const params = queryOptions(currentQuery);
|
||||
fetchJamTracks(params);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<FalconCardHeader title={t('search.page_title')} titleClass="font-weight-bold" />
|
||||
|
|
@ -126,6 +145,7 @@ const JKJamTracksFilter = () => {
|
|||
<Row>
|
||||
<Col>
|
||||
<JKJamTracksAutoComplete
|
||||
fetchFunc={autocompleteJamTracks}
|
||||
onSelect={handleOnSelect}
|
||||
onEnter={handleOnEnter}
|
||||
showDropdown={showDropdown}
|
||||
|
|
@ -153,8 +173,7 @@ const JKJamTracksFilter = () => {
|
|||
showArtists={showArtists}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<JKJamTracksList selectedType={selected?.type} searchTerm={searchTerm} jamTracks={jamTracks} nextPage={jamTracksNextPage} onNextPage={handleOnNextJamTracksPage} />
|
||||
<JKJamTracksList selectedType={selected?.type} searchTerm={searchTerm} jamTracks={jamTracks} nextOffset={nextOffset} onNextPage={handleOnNextJamTracksPage} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import React from 'react';
|
|||
import { Row, Col, Table, Button } from 'reactstrap';
|
||||
import JKJamTrackPreview from './JKJamTrackPreview';
|
||||
import JKJamTrackPurchaseButton from './JKJamTrackPurchaseButton';
|
||||
|
||||
import { JamTrackPreviewProvider } from '../../context/JamTrackPreviewContext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const JKJamTracksList = ({ selectedType, searchTerm, jamTracks, nextPage, onNextPage }) => {
|
||||
const JKJamTracksList = ({ selectedType, searchTerm, jamTracks, nextOffset, onNextPage }) => {
|
||||
return (
|
||||
<>
|
||||
{selectedType && searchTerm.length && jamTracks.length > 0 ? (
|
||||
|
|
@ -58,10 +58,10 @@ const JKJamTracksList = ({ selectedType, searchTerm, jamTracks, nextPage, onNext
|
|||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{nextPage && (
|
||||
{nextOffset && (
|
||||
<Row>
|
||||
<Col>
|
||||
<Button color="primary" onClick={onNextPage}>
|
||||
<Button color="primary" onClick={onNextPage} data-testid="moreBtn">
|
||||
Load More
|
||||
</Button>
|
||||
</Col>
|
||||
|
|
@ -71,4 +71,20 @@ const JKJamTracksList = ({ selectedType, searchTerm, jamTracks, nextPage, onNext
|
|||
);
|
||||
};
|
||||
|
||||
JKJamTracksList.propTypes = {
|
||||
selectedType: PropTypes.string || null,
|
||||
searchTerm: PropTypes.string,
|
||||
jamTracks: PropTypes.array,
|
||||
nextOffset: PropTypes.number,
|
||||
onNextPage: PropTypes.func,
|
||||
};
|
||||
|
||||
JKJamTracksList.defaultProps = {
|
||||
selectedType: null,
|
||||
searchTerm: '',
|
||||
jamTracks: [],
|
||||
nextOffset: null,
|
||||
onNextPage: () => {}
|
||||
};
|
||||
|
||||
export default JKJamTracksList;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Table } from 'reactstrap';
|
||||
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
||||
import { removeMixdown } from '../../store/features/jamTrackSlice';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
const JKMyJamTrackMixes = () => {
|
||||
const [mixes, setMixes] = useState([]);
|
||||
const fpPromise = FingerprintJS.load();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const jamTrack = useSelector(state => state.jamTrack.jamTrack);
|
||||
const mixdownsLoadingStatus = useSelector(state => state.jamTrack.mixdownsLoadingStatus);
|
||||
const deleteMixdownStatus = useSelector(state => state.jamTrack.deleteMixdownStatus);
|
||||
const tempMixdownLoadingStatus = useSelector(state => state.jamTrack.tempMixdownLoadingStatus);
|
||||
|
||||
useEffect(() => {
|
||||
if (!jamTrack) {
|
||||
return;
|
||||
}
|
||||
if (mixdownsLoadingStatus === 'succeeded') {
|
||||
setMixes(jamTrack.mixdowns.filter(m => m.id !== 'temp'));
|
||||
}
|
||||
}, [mixdownsLoadingStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tempMixdownLoadingStatus === 'succeeded') {
|
||||
setMixes(jamTrack.mixdowns);
|
||||
}
|
||||
}, [tempMixdownLoadingStatus]);
|
||||
|
||||
const downloadJamTrack = async () => {
|
||||
console.log('Downloading JamTrack');
|
||||
if (!jamTrack.can_download) {
|
||||
console.log('Cannot download JamTrack');
|
||||
return;
|
||||
}
|
||||
const fp = await fpPromise;
|
||||
const result = await fp.get();
|
||||
const src = `${process.env.REACT_APP_API_BASE_URL}/jamtracks/${
|
||||
jamTrack.id
|
||||
}/stems/master/download.mp3?file_type=mp3&download=1&mark=${result.visitorId}`;
|
||||
openDownload(src);
|
||||
};
|
||||
|
||||
const downloadMix = async mixId => {
|
||||
console.log('Download mixdown');
|
||||
const mixdown = mixes.find(m => m.id === mixId);
|
||||
const mixdownPackage = mixdown.packages.find(p => p.file_type === 'mp3');
|
||||
if (mixdownPackage?.signing_state == 'SIGNED') {
|
||||
const fp = await fpPromise;
|
||||
const result = await fp.get();
|
||||
const src = `${process.env.REACT_APP_API_BASE_URL}/mixdowns/${
|
||||
mixdown.id
|
||||
}/download.mp3?file_type=mp3&sample_rate=48&download=1&mark=${result.visitorId}`;
|
||||
openDownload(src);
|
||||
}else{
|
||||
console.log('Mixdown not signed');
|
||||
}
|
||||
};
|
||||
|
||||
const openDownload = async src => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = src;
|
||||
iframe.style.display = 'none';
|
||||
document.body.appendChild(iframe);
|
||||
};
|
||||
|
||||
const deleteMix = mixId => {
|
||||
if (window.confirm('Delete this custom mix?')) {
|
||||
console.log('Deleting mixdown', mixId);
|
||||
dispatch(removeMixdown({ id: mixId }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
You can save a <strong>maximum of 5 mixes</strong> on JamKazam. If you need to make more mixes, download a mix
|
||||
to save it, then delete it to make more room
|
||||
</p>
|
||||
<Table striped bordered className="fs--1">
|
||||
<thead className="bg-200 text-900">
|
||||
<tr>
|
||||
<th>Mix</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Full JamTrack</td>
|
||||
<td class="text-center">
|
||||
<a onClick={downloadJamTrack}>
|
||||
<FontAwesomeIcon icon="download" size="lg" className="mr-3" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{mixes.map(mix => (
|
||||
<tr key={mix.id}>
|
||||
<td>{mix.name}</td>
|
||||
<td class="text-center">
|
||||
{mix.id === 'temp' ? (
|
||||
<FontAwesomeIcon icon="spinner" size="lg" />
|
||||
) : (
|
||||
<>
|
||||
<a onClick={() => downloadMix(mix.id)} style={{ cursor: 'pointer' }}>
|
||||
<FontAwesomeIcon icon="download" size="lg" className="mr-3" />
|
||||
</a>
|
||||
<a
|
||||
onClick={() => deleteMix(mix.id)}
|
||||
disabled={deleteMixdownStatus === 'loading'}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<FontAwesomeIcon icon="trash" size="xl" />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKMyJamTrackMixes;
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Card, CardBody, ListGroup, ListGroupItem, FormGroup, Input, InputGroup, InputGroupText } from 'reactstrap';
|
||||
import FalconCardHeader from '../common/FalconCardHeader';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useResponsive } from '@farfetch/react-context-responsive';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import useOnScreen from '../../hooks/useOnScreen';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { fetchMyJamTracks, filterJamTracks } from '../../store/features/myJamTracksSlice';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
const JKMyJamTracks = () => {
|
||||
const { t } = useTranslation('jamtracks');
|
||||
const { greaterThan } = useResponsive();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const inputRef = React.createRef();
|
||||
const containerRef = useRef(null);
|
||||
const [lastJamTrackRef, setLastJamTrackRef] = useState(null);
|
||||
const isIntersecting = useOnScreen({ current: lastJamTrackRef });
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const jamTracks = useSelector(state => state.myJamTrack.jamTracks);
|
||||
const loadingStatus = useSelector(state => state.myJamTrack.status);
|
||||
const offset = useSelector(state => state.myJamTrack.next);
|
||||
|
||||
const handleInputChange = e => {
|
||||
const val = e.target.value;
|
||||
setInputValue(val);
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// dispatch(fetchMyJamTracks());
|
||||
// }, []);
|
||||
|
||||
useEffect(() => {
|
||||
const getMyJamTracks = setTimeout(async () => {
|
||||
await fetchJamTracks({ start: 0, search: inputValue, append: false });
|
||||
//dispatch(filterJamTracks(inputValue));
|
||||
}, 1000);
|
||||
return () => clearTimeout(getMyJamTracks);
|
||||
}, [inputValue]);
|
||||
|
||||
const fetchJamTracks = async params => {
|
||||
const { page } = params;
|
||||
try {
|
||||
dispatch(fetchMyJamTracks(params));
|
||||
} catch (error) {
|
||||
console.log('Error when fetching jam tracks', error);
|
||||
} finally {
|
||||
//setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isIntersecting) {
|
||||
if (offset && loadingStatus !== 'loading') {
|
||||
const params = { start: offset, search: inputValue, append: true };
|
||||
fetchJamTracks(params);
|
||||
}
|
||||
}
|
||||
}, [isIntersecting]);
|
||||
|
||||
const containerStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '400px',
|
||||
overflow: 'auto'
|
||||
};
|
||||
|
||||
return (
|
||||
<Card style={{ width: greaterThan.sm ? '50%' : '100%' }} className="mx-auto">
|
||||
<FalconCardHeader title={t('my.page_title')} titleClass="font-weight-semi-bold" />
|
||||
<CardBody className="pt-3">
|
||||
<FormGroup className="mb-3">
|
||||
<div className="d-flex align-items-center">
|
||||
<InputGroup>
|
||||
<InputGroupText style={{ borderRadius: '0', borderRight: '0' }}>
|
||||
{loadingStatus === 'loading' ? (
|
||||
<span className="spinner-grow spinner-grow-sm" aria-hidden="true" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon="search" transform="shrink-4 down-1" />
|
||||
)}
|
||||
</InputGroupText>
|
||||
|
||||
<Input
|
||||
onChange={handleInputChange}
|
||||
value={inputValue}
|
||||
innerRef={inputRef}
|
||||
placeholder={t('my.search_input.placeholder')}
|
||||
data-testid="autocomplete-text"
|
||||
type="search"
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<div style={containerStyle} ref={containerRef}>
|
||||
<ListGroup className="mt-1" data-testid="myJamTrackList">
|
||||
{jamTracks &&
|
||||
jamTracks.map((jamTrack, index) => (
|
||||
<div key={jamTrack.id} ref={ref => (jamTracks.length - 1 === index ? setLastJamTrackRef(ref) : null)}>
|
||||
<ListGroupItem>
|
||||
<Link to={`/jamtracks/${jamTrack.id}`}>{jamTrack.name}</Link>
|
||||
{jamTrack.original_artist && ` by ${jamTrack.original_artist}`}
|
||||
</ListGroupItem>
|
||||
</div>
|
||||
))}
|
||||
</ListGroup>
|
||||
{loadingStatus === 'loading' && <div className="d-flex justify-content-center"> Loading... </div>}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKMyJamTracks;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react'
|
||||
import ReactSlider from "react-slider";
|
||||
|
||||
const JKProgressSlider = () => {
|
||||
return (
|
||||
<ReactSlider
|
||||
className="horizontal-slider"
|
||||
thumbClassName="jamtrack-thumb"
|
||||
trackClassName="jamtrack-track"
|
||||
onSliderClick={(value) => console.log(value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default JKProgressSlider
|
||||
|
|
@ -1,20 +1,31 @@
|
|||
import classNames from 'classnames';
|
||||
import is from 'is_js';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Collapse, Nav, Navbar } from 'reactstrap';
|
||||
import bgNavbarImg from '../../assets/img/generic/bg-navbar.png';
|
||||
import { navbarBreakPoint, topNavbarBreakpoint } from '../../config';
|
||||
import AppContext from '../../context/Context';
|
||||
import routes from '../../routes';
|
||||
//import routes from '../../routes';
|
||||
import Flex from '../common/Flex';
|
||||
import Logo from './Logo';
|
||||
import NavbarTopDropDownMenus from './NavbarTopDropDownMenus';
|
||||
import NavbarVerticalMenu from './NavbarVerticalMenu';
|
||||
//import ToggleButton from './ToggleButton';
|
||||
|
||||
import { useAppRoutes } from '../../context/AppRoutesContext';
|
||||
|
||||
|
||||
const JKNavbarVertical = ({ navbarStyle }) => {
|
||||
|
||||
const { appRoutes } = useAppRoutes();
|
||||
const [routes, setRoutes] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if(appRoutes.length > 0){
|
||||
setRoutes(appRoutes);
|
||||
}
|
||||
}, [appRoutes]);
|
||||
|
||||
const navBarRef = useRef(null);
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { useAuth } from '../../context/UserAuth';
|
|||
const JKNotificationDropdown = () => {
|
||||
const { currentUser, isAuthenticated } = useAuth();
|
||||
const dispatch = useDispatch();
|
||||
const notifications = useSelector(state => state.notification.notifications.slice(0, 5));
|
||||
const notifications = useSelector(state => state.notification.notifications);
|
||||
|
||||
const LIMIT = 5;
|
||||
const [page, setPage] = useState(0);
|
||||
|
|
@ -34,8 +34,8 @@ const JKNotificationDropdown = () => {
|
|||
try {
|
||||
const options = {
|
||||
userId: currentUser.id,
|
||||
offset: page * LIMIT,
|
||||
limit: LIMIT
|
||||
offset: 0,
|
||||
limit: LIMIT + 1
|
||||
};
|
||||
await dispatch(fetchNotifications(options)).unwrap();
|
||||
//console.log('NOTIFICATIONS', notifications);
|
||||
|
|
@ -45,6 +45,10 @@ const JKNotificationDropdown = () => {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadNotifications();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadNotifications();
|
||||
|
|
@ -53,54 +57,60 @@ const JKNotificationDropdown = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{isAuthenticated &&
|
||||
<Dropdown
|
||||
nav
|
||||
inNavbar
|
||||
className="mx-3"
|
||||
isOpen={isOpen}
|
||||
toggle={handleToggle}
|
||||
// onMouseOver={() => {
|
||||
// let windowWidth = window.innerWidth;
|
||||
// windowWidth > 992 && setIsOpen(true);
|
||||
// }}
|
||||
// onMouseLeave={() => {
|
||||
// let windowWidth = window.innerWidth;
|
||||
// windowWidth > 992 && setIsOpen(false);
|
||||
// }}
|
||||
>
|
||||
<DropdownToggle
|
||||
nav
|
||||
className={classNames('px-0', {
|
||||
'notification-indicator notification-indicator-primary': !isAllRead
|
||||
})}
|
||||
>
|
||||
<FontAwesomeIcon icon="bell" transform="shrink-6" className="fs-4" />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu right className="dropdown-menu-card" data-testid="notificationDropdown">
|
||||
<Card className="card-notification shadow-none" style={{ maxWidth: '20rem' }}>
|
||||
<FalconCardHeader className="card-header" title="Notifications" titleTag="h6" light={false}>
|
||||
{/* <Link className="card-link font-weight-normal" to="#!">
|
||||
{isAuthenticated && (
|
||||
<Dropdown
|
||||
nav
|
||||
inNavbar
|
||||
className="mx-3"
|
||||
isOpen={isOpen}
|
||||
toggle={handleToggle}
|
||||
// onMouseOver={() => {
|
||||
// let windowWidth = window.innerWidth;
|
||||
// windowWidth > 992 && setIsOpen(true);
|
||||
// }}
|
||||
// onMouseLeave={() => {
|
||||
// let windowWidth = window.innerWidth;
|
||||
// windowWidth > 992 && setIsOpen(false);
|
||||
// }}
|
||||
>
|
||||
<DropdownToggle
|
||||
nav
|
||||
className={classNames('px-0', {
|
||||
'': !isAllRead
|
||||
})}
|
||||
>
|
||||
{ isIterableArray(notifications) && notifications.length > 0 && <div className="num-circle">
|
||||
{ notifications.length > LIMIT ? `${LIMIT}+` : notifications.length}
|
||||
</div> }
|
||||
<FontAwesomeIcon icon={['fas', 'bell']} transform="shrink-5" className="fs-4" />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu right className="dropdown-menu-card" data-testid="notificationDropdown">
|
||||
<Card className="card-notification shadow-none" style={{ maxWidth: '20rem' }}>
|
||||
<FalconCardHeader className="card-header" title="Notifications" titleTag="h6" light={false}>
|
||||
{/* <Link className="card-link font-weight-normal" to="#!">
|
||||
Mark all as read
|
||||
</Link> */}
|
||||
</FalconCardHeader>
|
||||
<ListGroup flush className="font-weight-normal fs--1">
|
||||
{isIterableArray(notifications) &&
|
||||
notifications.map(notification => (
|
||||
<ListGroupItem key={`notification-drop-item-${notification.notification_id}`} onClick={handleToggle}>
|
||||
<Notification notification={notification} classNames="bg-200" flush />
|
||||
</ListGroupItem>
|
||||
))}
|
||||
</ListGroup>
|
||||
<div className="card-footer text-center border-top" onClick={handleToggle}>
|
||||
<Link className="card-link d-block" to="/notifications">
|
||||
View all
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
}
|
||||
</FalconCardHeader>
|
||||
<ListGroup flush className="font-weight-normal fs--1">
|
||||
{isIterableArray(notifications) &&
|
||||
notifications.slice(0, LIMIT).map(notification => (
|
||||
<ListGroupItem
|
||||
key={`notification-drop-item-${notification.notification_id}`}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<Notification notification={notification} classNames="bg-200" flush />
|
||||
</ListGroupItem>
|
||||
))}
|
||||
</ListGroup>
|
||||
<div className="card-footer text-center border-top" onClick={handleToggle}>
|
||||
<Link className="card-link d-block" to="/notifications">
|
||||
View all
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ const ProfileDropdown = () => {
|
|||
// }}
|
||||
>
|
||||
<DropdownToggle nav className="pr-0">
|
||||
<JKProfileAvatar url={currentUser.photo_url} className="d-block d-lg-none d-xl-none" />
|
||||
<span className="d-none d-lg-block">{currentUser && currentUser.name}</span>
|
||||
<JKProfileAvatar src={currentUser.photo_url} className="d-block d-lg-none d-xl-none" />
|
||||
{/* <span className="d-none d-lg-block">{currentUser && currentUser.name}</span> */}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu right className="dropdown-menu-card">
|
||||
<div className="bg-white rounded-soft py-2">
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@ const TopNavRightSideNavItem = () => {
|
|||
</NavItem> */}
|
||||
|
||||
<NavbarText className="d-none d-md-inline">{t('keep_jamkazam_improving', { ns: 'common' })}:</NavbarText>
|
||||
<NavItem className="d-none d-md-inline ml-1 mr-6">
|
||||
<NavItem className="d-none d-md-inline ml-1 mr-2">
|
||||
<a className="nav-link" href={`${process.env.REACT_APP_CLIENT_BASE_URL}/client#/account/subscription`} target="_blank">
|
||||
{t('subscribe', { ns: 'common' })}
|
||||
</a>
|
||||
</NavItem>
|
||||
|
||||
<LangSwitch />
|
||||
{/* <LangSwitch /> */}
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<NotificationDropdown />
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const NavbarVerticalMenu = ({ routes, location }) => {
|
|||
navs.push({ ...route, isOpened: true, index })
|
||||
});
|
||||
setNavRoutes(navs)
|
||||
}, []);
|
||||
}, [routes]);
|
||||
|
||||
const toggleOpened = (e, route) => {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import {
|
|||
getCities
|
||||
} from '../../helpers/rest';
|
||||
import JKProfileAvatarUpload from '../profile/JKProfileAvatarUpload';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Prompt } from 'react-router';
|
||||
|
||||
function JKEditProfile() {
|
||||
const { t } = useTranslation('profile');
|
||||
|
|
@ -29,7 +31,7 @@ function JKEditProfile() {
|
|||
const [regions, setRegions] = useState([]);
|
||||
const [cities, setCities] = useState([]);
|
||||
const [showAvatarUpload, setShowAvatarUpload] = useState(false);
|
||||
//const [userData, setUserData] = useState({});
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
const [_, forceUpdate] = useReducer(x => x + 1, 0);
|
||||
|
||||
|
|
@ -43,7 +45,7 @@ function JKEditProfile() {
|
|||
{ value: '3', label: t('profeciency.advanced') }
|
||||
];
|
||||
|
||||
const { register, control, handleSubmit, setValue, getValues } = useForm({
|
||||
const { register, control, handleSubmit, setValue, getValues, isDirty } = useForm({
|
||||
defaultValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
|
|
@ -64,7 +66,7 @@ function JKEditProfile() {
|
|||
if (currentUser && !currentUserLoaded) {
|
||||
setCurrentUserLoaded(true);
|
||||
fetchCurentUser().then(data => {
|
||||
console.log("userData", data)
|
||||
console.log('userData', data);
|
||||
updateUserData(data);
|
||||
fetchInstruments();
|
||||
fetchGenres();
|
||||
|
|
@ -269,7 +271,7 @@ function JKEditProfile() {
|
|||
const updatedMusicInstruments = musicInstruments.map(instrument => {
|
||||
if (instrument.id === musicInstrument.id) {
|
||||
instrument.proficiency_level = option.value;
|
||||
}
|
||||
}
|
||||
return instrument;
|
||||
});
|
||||
|
||||
|
|
@ -305,6 +307,7 @@ function JKEditProfile() {
|
|||
};
|
||||
|
||||
const handleTextInputChage = () => {
|
||||
setUpdating(true);
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
saveTimeoutRef.current = setTimeout(() => {
|
||||
handleChange();
|
||||
|
|
@ -329,14 +332,13 @@ function JKEditProfile() {
|
|||
};
|
||||
|
||||
const handleRegionChange = selectedOpt => {
|
||||
console.log("region selectedOpt", selectedOpt)
|
||||
if (!selectedOpt) return;
|
||||
|
||||
|
||||
if (skipRegionChange.current) {
|
||||
skipRegionChange.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const state = selectedOpt.value;
|
||||
const country = getValues('country');
|
||||
setValue('state', state);
|
||||
|
|
@ -378,6 +380,7 @@ function JKEditProfile() {
|
|||
|
||||
data.instruments = instrments;
|
||||
|
||||
setUpdating(true);
|
||||
updateUser(currentUser.id, data)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
|
|
@ -386,21 +389,43 @@ function JKEditProfile() {
|
|||
console.log('Error updating user data');
|
||||
}
|
||||
})
|
||||
.catch(error => console.log(error));
|
||||
.catch(error => console.log(error))
|
||||
.finally(() => {
|
||||
setUpdating(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function beforeUnload(e) {
|
||||
if (updating) e.preventDefault();
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', beforeUnload);
|
||||
};
|
||||
}, [updating]);
|
||||
|
||||
const toggleAvatarUpload = () => {
|
||||
setShowAvatarUpload(!showAvatarUpload);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Prompt when={updating} message="The changes are being saved. Are you sure you want to leave?" />
|
||||
<Card>
|
||||
<FalconCardHeader title={t('page_title', { ns: 'profile' })} titleClass="font-weight-bold" />
|
||||
<FalconCardHeader title={t('page_title', { ns: 'profile' })} titleClass="font-weight-bold">
|
||||
{updating && (
|
||||
<>
|
||||
<FontAwesomeIcon icon="spinner" spin className="ml-2" /> <span>updating...</span>
|
||||
</>
|
||||
)}
|
||||
</FalconCardHeader>
|
||||
<CardBody className="pt-3" style={{ backgroundColor: '#edf2f9' }}>
|
||||
<Form data-testid="edit_profile_form">
|
||||
<Row>
|
||||
<Col>
|
||||
<Col sm="12" md="6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h5>{t('basics')}</h5>
|
||||
|
|
@ -445,23 +470,24 @@ function JKEditProfile() {
|
|||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<div className="d-flex align-items-center">
|
||||
<div>
|
||||
<JKProfileAvatar src={currentUser.photo_url} size="s" />
|
||||
<Col md={4} className='d-flex flex-column'>
|
||||
<a
|
||||
href="#"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
toggleAvatarUpload();
|
||||
}}
|
||||
style={{ marginTop: "auto", marginBottom: "auto" }}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<div>
|
||||
<JKProfileAvatar src={currentUser.photo_url} size="3xl" />
|
||||
</div>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={['fas', 'edit']} className="ml-2 mr-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="#"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
toggleAvatarUpload();
|
||||
}}
|
||||
>
|
||||
{t('change_photo')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
|
|
@ -503,39 +529,38 @@ function JKEditProfile() {
|
|||
<Col md={4}>
|
||||
<FormGroup>
|
||||
<Label for="state">{t('state')}</Label>
|
||||
|
||||
<Controller
|
||||
name="state"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const region = regions.find(region => region.region === value);
|
||||
if (region) {
|
||||
|
||||
<Controller
|
||||
name="state"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const region = regions.find(region => region.region === value);
|
||||
if (region) {
|
||||
return (
|
||||
<Select
|
||||
isDisabled={getValues('country') === null || regions.length === 0}
|
||||
value={{ value: region.region, label: region.region }}
|
||||
value={{ value: region.region, label: region.name }}
|
||||
ref={regionRef}
|
||||
onChange={handleRegionChange}
|
||||
options={regions.map(r => {
|
||||
return { value: r.region, label: r.region };
|
||||
return { value: r.region, label: r.name };
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}else{
|
||||
return (
|
||||
<Select
|
||||
isDisabled={getValues('country') === null || regions.length === 0}
|
||||
ref={regionRef}
|
||||
onChange={handleRegionChange}
|
||||
options={regions.map(r => {
|
||||
return { value: r.region, label: r.region };
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
} else {
|
||||
return (
|
||||
<Select
|
||||
isDisabled={getValues('country') === null || regions.length === 0}
|
||||
ref={regionRef}
|
||||
onChange={handleRegionChange}
|
||||
options={regions.map(r => {
|
||||
return { value: r.region, label: r.name };
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
|
|
@ -669,51 +694,8 @@ function JKEditProfile() {
|
|||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h5>{t('instruments')}</h5>
|
||||
</CardHeader>
|
||||
<CardBody data-testid="instruments" className="bg-light" style={{ overflowY: 'scroll', height: 300 }}>
|
||||
<FormGroup check>
|
||||
{instrumentsInitialLoadingDone &&
|
||||
musicInstruments.map((musicInstrument, index) => {
|
||||
return (
|
||||
<Row key={musicInstrument.id} className="mb-1">
|
||||
<Col md={4}>
|
||||
<Input
|
||||
onChange={e => {
|
||||
handleInstrumentSelect(e, musicInstrument);
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={musicInstrument.checked}
|
||||
/>
|
||||
|
||||
<Label check for="check">
|
||||
{musicInstrument.description}
|
||||
</Label>
|
||||
</Col>
|
||||
<Col md={3}>
|
||||
<Select
|
||||
value={
|
||||
musicInstrument.checked
|
||||
? PROFICIENCIES.find(p => parseInt(p.value) === musicInstrument.proficiency_level)
|
||||
: null
|
||||
}
|
||||
onChange={e => {
|
||||
handleInstrumentProficiencyChange(e, musicInstrument);
|
||||
}}
|
||||
options={PROFICIENCIES}
|
||||
isDisabled={!musicInstrument.checked}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</FormGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card className="mt-3">
|
||||
<Col sm="12" md="6">
|
||||
<Card className="mt-3 mt-md-0">
|
||||
<CardHeader>
|
||||
<h5>{t('genres')}</h5>
|
||||
</CardHeader>
|
||||
|
|
@ -741,6 +723,49 @@ function JKEditProfile() {
|
|||
</FormGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card className="mt-3">
|
||||
<CardHeader>
|
||||
<h5>{t('instruments')}</h5>
|
||||
</CardHeader>
|
||||
<CardBody data-testid="instruments" className="bg-light" style={{ overflowY: 'scroll', height: 300 }}>
|
||||
<FormGroup check>
|
||||
{instrumentsInitialLoadingDone &&
|
||||
musicInstruments.map((musicInstrument, index) => {
|
||||
return (
|
||||
<Row key={musicInstrument.id} className="mb-1">
|
||||
<Col md={5}>
|
||||
<Input
|
||||
onChange={e => {
|
||||
handleInstrumentSelect(e, musicInstrument);
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={musicInstrument.checked}
|
||||
/>
|
||||
|
||||
<Label check for="check">
|
||||
{musicInstrument.description}
|
||||
</Label>
|
||||
</Col>
|
||||
<Col md={7}>
|
||||
<Select
|
||||
value={
|
||||
musicInstrument.checked
|
||||
? PROFICIENCIES.find(p => parseInt(p.value) === musicInstrument.proficiency_level)
|
||||
: null
|
||||
}
|
||||
onChange={e => {
|
||||
handleInstrumentProficiencyChange(e, musicInstrument);
|
||||
}}
|
||||
options={PROFICIENCIES}
|
||||
isDisabled={!musicInstrument.checked}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</FormGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import defaultAvatarUrl from '../../assets/img/team/avatar.png';
|
|||
import Avatar from '../common/Avatar';
|
||||
|
||||
const JKProfileAvatar = (options) => {
|
||||
const {src, ...rest} = options;
|
||||
const {src, size, ...rest} = options;
|
||||
const avatarUrl = () => {
|
||||
if (src) {
|
||||
return src;
|
||||
|
|
@ -13,7 +13,7 @@ const JKProfileAvatar = (options) => {
|
|||
}
|
||||
};
|
||||
|
||||
return <Avatar src={avatarUrl()} rest />;
|
||||
return <Avatar src={avatarUrl()} size={size} rest />;
|
||||
};
|
||||
|
||||
JKProfileAvatar.propTypes = {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import JKModalDialog from '../common/JKModalDialog';
|
||||
import JKProfileAvatar from './JKProfileAvatar';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
import { getUserDetails } from '../../helpers/rest';
|
||||
import { getUserDetail } from '../../helpers/rest';
|
||||
|
||||
const JKProfileAvatarUpload = ({show, toggle}) => {
|
||||
const { t } = useTranslation('profile');
|
||||
|
|
@ -14,7 +14,7 @@ const JKProfileAvatarUpload = ({show, toggle}) => {
|
|||
useEffect(() => {
|
||||
if(currentUser) {
|
||||
console.log(currentUser.photo_url);
|
||||
// getUserDetails(currentUser.id).then(response => {
|
||||
// getUserDetail(currentUser.id).then(response => {
|
||||
// console.log('_userDetails', response);
|
||||
// });
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ const JKProfileAvatarUpload = ({show, toggle}) => {
|
|||
<JKModalDialog
|
||||
show={show}
|
||||
onToggle={toggle}
|
||||
title={t('lobby.chat_notifications.title', { ns: 'sessions' })}
|
||||
title={t('photo_modal.title', { ns: 'profile' })}
|
||||
showFooter={true}
|
||||
>
|
||||
<div className='d-flex flex-column'>
|
||||
|
|
@ -42,8 +42,8 @@ const JKProfileAvatarUpload = ({show, toggle}) => {
|
|||
<JKProfileAvatar src={currentUser.photo_url} size="5xl" />
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-center">
|
||||
<Button color="secondary" outline className="ml-2" onClick={openFilePicker}>
|
||||
<div className="d-flex justify-content-center mt-2">
|
||||
<Button color="primary" className="ml-2" onClick={openFilePicker}>
|
||||
{t('photo_modal.upload', { ns: 'profile' })}
|
||||
</Button>
|
||||
<Button color="secondary" outline className="ml-2" onClick={() => {}}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,665 @@
|
|||
import React, { useState, useContext, useEffect, useMemo } from 'react';
|
||||
import ContentWithAsideLayout from '../../layouts/ContentWithAsideLayout';
|
||||
import AppContext from '../../context/Context';
|
||||
import CheckoutAside from './checkout/CheckoutAside';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
Col,
|
||||
Button,
|
||||
Row,
|
||||
FormGroup,
|
||||
Input,
|
||||
CustomInput,
|
||||
UncontrolledTooltip,
|
||||
Label
|
||||
} from 'reactstrap';
|
||||
import Select from 'react-select';
|
||||
import FalconCardHeader from '../common/FalconCardHeader';
|
||||
import { useForm, Controller, get, set } from 'react-hook-form';
|
||||
import FalconInput from '../common/FalconInput';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import Flex from '../common/Flex';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import iconPaymentMethodsGrid from '../../assets/img/icons/icon-payment-methods-grid.png';
|
||||
import iconPaypalFull from '../../assets/img/icons/icon-paypal-full.png';
|
||||
import { useResponsive } from '@farfetch/react-context-responsive';
|
||||
import { useShoppingCart } from '../../hooks/useShoppingCart';
|
||||
import { getBillingInfo, getUserDetail, getCountries, createRecurlyAccount, placeOrder } from '../../helpers/rest';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
import { isValid, isExpirationDateValid, isSecurityCodeValid, getCreditCardNameByNumber } from 'creditcard.js';
|
||||
import { useCheckout } from '../../hooks/useCheckout';
|
||||
|
||||
const JKCheckout = () => {
|
||||
const { currency } = useContext(AppContext);
|
||||
const { cartTotal: payableTotal, loading: cartLoading } = useShoppingCart();
|
||||
const { greaterThan } = useResponsive();
|
||||
const { currentUser } = useAuth();
|
||||
const history = useHistory();
|
||||
const { setPreserveBillingInfo, refreshPreserveBillingInfo, shouldPreserveBillingInfo, deletePreserveBillingInfo } = useCheckout();
|
||||
|
||||
const [paymentMethod, setPaymentMethod] = useState('credit-card');
|
||||
const [paymentErrorMessage, setPaymentErrorMessage] = useState('');
|
||||
const [orderErrorMessage, setOrderErrorMessage] = useState('');
|
||||
const [cardNumber, setCardNumber] = useState('');
|
||||
const [billingInfo, setBillingInfo] = useState({});
|
||||
const [countries, setCountries] = useState([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const [reuseExistingCard, setReuseExistingCard] = useState(false);
|
||||
const [hasRedeemableJamTrack, setHasRedeemableJamTrack] = useState(false);
|
||||
const [hasAlreadyEnteredBillingInfo, setHasAlreadyEnteredBillingInfo] = useState(false);
|
||||
const [saveThisCard, setSaveThisCard] = useState(false);
|
||||
const [hideBillingInfo, setHideBillingInfo] = useState(true);
|
||||
|
||||
const labelClassName = 'ls text-600 font-weight-semi-bold mb-0';
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
setError,
|
||||
formState: { errors }
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
address1: '',
|
||||
address2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
country: 'US',
|
||||
number: '',
|
||||
month: '',
|
||||
year: '',
|
||||
verification_value: ''
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldPreserveBillingInfo) {
|
||||
refreshPreserveBillingInfo();
|
||||
setHasAlreadyEnteredBillingInfo(true);
|
||||
setHideBillingInfo(true);
|
||||
} else {
|
||||
setHideBillingInfo(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser) {
|
||||
fetchCountries();
|
||||
populateData();
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
const fetchCountries = () => {
|
||||
getCountries()
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
setCountries(data.countriesx);
|
||||
})
|
||||
.catch(error => console.log(error));
|
||||
};
|
||||
|
||||
const populateData = async () => {
|
||||
const options = {
|
||||
id: currentUser.id
|
||||
};
|
||||
try {
|
||||
const userResp = await getUserDetail(options);
|
||||
const userData = await userResp.json();
|
||||
console.log('User Data:', userData);
|
||||
setHasRedeemableJamTrack(userData.has_redeemable_jamtrack);
|
||||
|
||||
if (userData.has_recurly_account) {
|
||||
setReuseExistingCard(userData.reuse_card);
|
||||
await populateBillingAddress();
|
||||
} else {
|
||||
setValue('first_name', userData.first_name);
|
||||
setValue('last_name', userData.last_name);
|
||||
setValue('address1', userData.address1);
|
||||
setValue('address2', userData.address2);
|
||||
setValue('city', userData.city);
|
||||
setValue('state', userData.state);
|
||||
setValue('zip', userData.zip);
|
||||
setValue('country', userData.country);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get user details:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const populateBillingAddress = async () => {
|
||||
try {
|
||||
const resp = await getBillingInfo();
|
||||
const data = await resp.json();
|
||||
const bi = data.billing_info;
|
||||
|
||||
setValue('first_name', bi.first_name);
|
||||
setValue('last_name', bi.last_name);
|
||||
setValue('address1', bi.address1);
|
||||
setValue('address2', billingInfo.address2);
|
||||
setValue('city', bi.city);
|
||||
setValue('state', bi.state);
|
||||
setValue('zip', bi.zip);
|
||||
setValue('country', bi.country);
|
||||
|
||||
setBillingInfo(bi);
|
||||
} catch (error) {
|
||||
console.error('Failed to get billing info:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const disableCardFields = useMemo(() => {
|
||||
return paymentMethod === 'existing-card';
|
||||
}, [paymentMethod]);
|
||||
|
||||
const onSubmit = async data => {
|
||||
console.log('Form Data:', data);
|
||||
if (paymentMethod === 'credit-card' || paymentMethod === 'existing-card') {
|
||||
constructRecurlyAccount(data);
|
||||
} else if (paymentMethod === 'paypal') {
|
||||
handoverToPaypal();
|
||||
}
|
||||
};
|
||||
|
||||
const constructRecurlyAccount = async data => {
|
||||
console.log('Form Data:', data);
|
||||
|
||||
if (paymentMethod === 'credit-card' && !isValidateCard(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bInfo = { ...data, number: cardNumber };
|
||||
// Save card
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await createRecurlyAccount({
|
||||
billing_info: bInfo,
|
||||
terms_of_service: true,
|
||||
reuse_card_this_time: paymentMethod === 'existing-card',
|
||||
reuse_card_next_time: saveThisCard || paymentMethod === 'existing-card'
|
||||
});
|
||||
setPreserveBillingInfo();
|
||||
await doPlaceOrder();
|
||||
} catch (error) {
|
||||
console.error('Failed to create recurly account:', error);
|
||||
if (error.responseJSON && error.responseJSPN.errors) {
|
||||
error.responseJSON.errors.forEach((key, err) => {
|
||||
if (key === 'number') {
|
||||
setError('number', { type: 'manual', message: err }, { shouldFocus: false });
|
||||
}
|
||||
if (key === 'verification_value') {
|
||||
setError('verification_value', { type: 'manual', message: err }, { shouldFocus: false });
|
||||
}
|
||||
if (key === 'message') {
|
||||
setPaymentErrorMessage(err);
|
||||
}
|
||||
});
|
||||
} else if (error.responseText) {
|
||||
setPaymentErrorMessage(error.responseText);
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const doPlaceOrder = async () => {
|
||||
let message = 'Error submitting payment: ';
|
||||
try {
|
||||
const orderResp = await placeOrder();
|
||||
const orderData = await orderResp.json();
|
||||
console.log('Order Data:', orderData);
|
||||
localStorage.setItem('lastPurchaseResponse', JSON.stringify(orderData));
|
||||
deletePreserveBillingInfo();
|
||||
history.push('/checkout/success');
|
||||
} catch (error) {
|
||||
console.error('Failed to place order:', error);
|
||||
if (error.responseJSON && error.responseJSON.errors) {
|
||||
error.responseJSON.errors.forEach((key, err) => {
|
||||
message += key + ': ' + err;
|
||||
});
|
||||
setOrderErrorMessage(message);
|
||||
} else if (error.responseText) {
|
||||
setOrderErrorMessage(error.responseText);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isValidateCard = data => {
|
||||
let _isValid = true;
|
||||
|
||||
if (!isValid(cardNumber)) {
|
||||
_isValid = false;
|
||||
console.log('Invalid Card Number');
|
||||
setError('number', { type: 'manual', message: 'Invalid Card Number' }, { shouldFocus: false });
|
||||
}
|
||||
if (!isExpirationDateValid(data.month, data.year)) {
|
||||
_isValid = false;
|
||||
console.log('Invalid Expiration Date');
|
||||
setError('month', { type: 'manual', message: 'Invalid Expiration Date' }, { shouldFocus: false });
|
||||
setError('year', { type: 'manual', message: 'Invalid Expiration Date' }, { shouldFocus: false });
|
||||
}
|
||||
// if (!isSecurityCodeValid(data.verification_value)) {
|
||||
// _isValid = false;
|
||||
// console.log('Invalid Security Code');
|
||||
// setError('verification_value', { type: 'manual', message: 'Invalid Security Code' }, { shouldFocus: false });
|
||||
// }
|
||||
return _isValid;
|
||||
};
|
||||
|
||||
function formatCardNumber(value) {
|
||||
const v = value.replace(/\s+/g, '').replace(/[^0-9]/gi, '');
|
||||
const matches = v.match(/\d{4,16}/g);
|
||||
const match = (matches && matches[0]) || '';
|
||||
const parts = [];
|
||||
|
||||
for (let i = 0; i < match.length; i += 4) {
|
||||
parts.push(match.substring(i, i + 4));
|
||||
}
|
||||
|
||||
if (parts.length) {
|
||||
return parts.join(' ');
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
const handleOnCardNumberChange = e => {
|
||||
const cardNumber = e.target.value;
|
||||
console.log('Formatted Card Number:', formatCardNumber(cardNumber));
|
||||
setCardNumber(formatCardNumber(cardNumber));
|
||||
};
|
||||
|
||||
const handoverToPaypal = () => {
|
||||
// Handover to Paypal
|
||||
window.location = `${process.env.REACT_APP_CLIENT_BASE_URL}/paypal/checkout/start`;
|
||||
};
|
||||
|
||||
const handleCountryChange = selectedOption => {
|
||||
setValue('country', selectedOption.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ width: greaterThan.sm ? '50%' : '100%' }} className="mx-auto">
|
||||
<ContentWithAsideLayout
|
||||
aside={cartLoading ? <div>Cart Loading...</div> : <CheckoutAside />}
|
||||
isStickyAside={false}
|
||||
>
|
||||
{hasAlreadyEnteredBillingInfo && (
|
||||
<div className="alert alert-info" role="alert">
|
||||
<div className="d-flex">
|
||||
<FontAwesomeIcon icon="info-circle" className="mr-2" />
|
||||
<p>
|
||||
You recently entered payment info successfully. If you want to change your payment info, click the
|
||||
CHANGE PAYMENT INFO button. Otherwise, click the Confirm & Pay button to checkout.
|
||||
</p>
|
||||
</div>
|
||||
<div className='d-flex'>
|
||||
<Button onClick={() => setHideBillingInfo(!hideBillingInfo)}>
|
||||
{hideBillingInfo ? 'Change Payment Info' : 'Hide Payment Info'}
|
||||
</Button>
|
||||
<Button onClick={ doPlaceOrder } className="ml-2">
|
||||
Confirm & Pay
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={hideBillingInfo ? 'd-none' : 'd-block'}>
|
||||
{hasRedeemableJamTrack ? (
|
||||
<div className="alert alert-info d-flex" role="alert">
|
||||
<FontAwesomeIcon icon="info-circle" className="mr-2" />
|
||||
<p>
|
||||
Please enter your billing address and payment information below. {' '}
|
||||
<strong>You will not be billed for any charges of any kind without your explicit authorization.</strong>
|
||||
There are no "hidden" charges or fees, thank you!
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="alert alert-info d-flex" role="alert">
|
||||
<FontAwesomeIcon icon="info-circle" className="mr-2" />
|
||||
<p>Please enter your billing address and payment information below. </p>
|
||||
</div>
|
||||
)}
|
||||
<Card className="mb-3">
|
||||
<FalconCardHeader title="Billing Address" titleTag="h5" />
|
||||
<CardBody>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="first_name" className={labelClassName}>
|
||||
First Name
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<input {...register('first_name', { required: 'First Name is required' })} className="form-control" />
|
||||
{errors.first_name && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.first_name.message}</small>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="last_name" className={labelClassName}>
|
||||
Last Name
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<input {...register('last_name', { required: 'Last Name is required' })} className="form-control" />
|
||||
{errors.last_name && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.last_name.message}</small>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="address1" className={labelClassName}>
|
||||
Address 1
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<input {...register('address1', { required: 'Address is required' })} className="form-control" />
|
||||
{errors.address1 && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.address1.message}</small>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="address2" className={labelClassName}>
|
||||
Address 2
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<input {...register('address2')} className="form-control" />
|
||||
{errors.address2 && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.address2.message}</small>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="city" className={labelClassName}>
|
||||
City
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<input {...register('city', { required: 'City is required' })} className="form-control" />
|
||||
{errors.city && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.city.message}</small>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="state" className={labelClassName}>
|
||||
State or Region
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<input {...register('state', { required: 'State or Region is required' })} className="form-control" />
|
||||
{errors.state && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.state.message}</small>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="zip" className={labelClassName}>
|
||||
Zip or Postal Code
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<input
|
||||
{...register('zip', { required: 'Zip or Postal Code is required' })}
|
||||
className="form-control"
|
||||
/>
|
||||
{errors.zip && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.zip.message}</small>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="country" className={labelClassName}>
|
||||
Country
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Controller
|
||||
name="country"
|
||||
control={control}
|
||||
rules={{ required: 'Country is required' }}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const country = countries.find(country => country.countrycode === value);
|
||||
if (!country) {
|
||||
return (
|
||||
<Select
|
||||
data-testid="countrySelect"
|
||||
onChange={handleCountryChange}
|
||||
options={countries.map(c => {
|
||||
return { value: c.countrycode, label: c.countryname };
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
data-testid="countrySelect"
|
||||
value={{ value: country.countrycode, label: country.countryname }}
|
||||
onChange={handleCountryChange}
|
||||
options={countries.map(c => {
|
||||
return { value: c.countrycode, label: c.countryname };
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.country && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.country.message}</small>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card className="mb-3">
|
||||
<FalconCardHeader title="Payment Method" titleTag="h5" />
|
||||
<CardBody>
|
||||
{paymentErrorMessage && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{paymentErrorMessage}
|
||||
</div>
|
||||
)}
|
||||
{reuseExistingCard && (
|
||||
<>
|
||||
<Row className="mt-3">
|
||||
<Col xs={12}>
|
||||
<CustomInput
|
||||
label={
|
||||
<>
|
||||
<Flex align="center" className="mb-2">
|
||||
<div className="fs-1">Reuse Existing Card</div>
|
||||
</Flex>
|
||||
<div>Use card ending with {billingInfo.last_four}</div>
|
||||
</>
|
||||
}
|
||||
id="existing-card"
|
||||
value="existing-card"
|
||||
checked={paymentMethod === 'existing-card'}
|
||||
onChange={({ target }) => setPaymentMethod(target.value)}
|
||||
type="radio"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<CustomInput
|
||||
label={
|
||||
<Flex align="center" className="mb-2 fs-1">
|
||||
Credit Card
|
||||
</Flex>
|
||||
}
|
||||
id="credit-card"
|
||||
value="credit-card"
|
||||
checked={paymentMethod === 'credit-card'}
|
||||
onChange={({ target }) => setPaymentMethod(target.value)}
|
||||
type="radio"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} className="pl-4">
|
||||
<Row>
|
||||
<Col sm={8}>
|
||||
<Row className="align-items-center">
|
||||
<Col>
|
||||
<FormGroup>
|
||||
<input
|
||||
type="text"
|
||||
value={cardNumber}
|
||||
className={errors.number ? 'form-control form-control-is-invalid' : 'form-control'}
|
||||
placeholder="•••• •••• •••• ••••"
|
||||
onChange={handleOnCardNumberChange}
|
||||
disabled={disableCardFields}
|
||||
/>
|
||||
{/* {errors.number && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.number.message}</small>
|
||||
</div>
|
||||
)} */}
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="align-items-center">
|
||||
<Col xs={4}>
|
||||
<FormGroup>
|
||||
<Label>Month</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register('month')}
|
||||
className={errors.month ? 'form-control form-control-is-invalid' : 'form-control'}
|
||||
placeholder="MM"
|
||||
maxLength={2}
|
||||
disabled={disableCardFields}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<FormGroup>
|
||||
<Label>Year</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register('year')}
|
||||
className={errors.year ? 'form-control form-control-is-invalid' : 'form-control'}
|
||||
placeholder="YYYY"
|
||||
maxLength={4}
|
||||
disabled={disableCardFields}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<FormGroup>
|
||||
<Label>CVV</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register('verification_value')}
|
||||
className={
|
||||
errors.verification_value ? 'form-control form-control-is-invalid' : 'form-control'
|
||||
}
|
||||
placeholder="123"
|
||||
maxLength={3}
|
||||
disabled={disableCardFields}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<FormGroup check>
|
||||
<Label check>
|
||||
<Input
|
||||
type="checkbox"
|
||||
checked={saveThisCard}
|
||||
onChange={() => setSaveThisCard(!saveThisCard)}
|
||||
/>{' '}
|
||||
Reuse this card for future purchases
|
||||
</Label>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<div className="col-4 text-center pt-2 d-none d-sm-block">
|
||||
<div className="rounded p-2 mt-3 bg-100">
|
||||
<div className="text-uppercase fs--2 font-weight-bold">We Accept</div>
|
||||
<img src={iconPaymentMethodsGrid} alt="" width="120" />
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
<Row className="mt-3">
|
||||
<Col xs={12}>
|
||||
<CustomInput
|
||||
label={<img className="pull-right" src={iconPaypalFull} height="20" alt="" />}
|
||||
id="paypal"
|
||||
value="paypal"
|
||||
checked={paymentMethod === 'paypal'}
|
||||
onChange={({ target }) => setPaymentMethod(target.value)}
|
||||
type="radio"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr className="border-dashed my-5" />
|
||||
<Row>
|
||||
<Col className="pl-lg-4 pl-xl-2 pl-xxl-5 text-center">
|
||||
<hr className="border-dashed d-block d-md-none d-xl-block d-xxl-none my-4" />
|
||||
<div className="fs-2 font-weight-semi-bold">
|
||||
All Total:{' '}
|
||||
<span className="text-primary">
|
||||
{currency}
|
||||
{payableTotal}
|
||||
</span>
|
||||
</div>
|
||||
<Button type="submit" color="primary" className="mt-3 px-5" disabled={!payableTotal || submitting}>
|
||||
Confirm & Pay
|
||||
</Button>
|
||||
<p className="fs--1 mt-3 mb-0">
|
||||
By clicking <strong>Confirm & Pay </strong>button you agree to JamKazam's{' '}
|
||||
<Link to="#!">Terms of service</Link>, including the <Link to="#!">JamTracks purchase terms</Link>.
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</form>
|
||||
</ContentWithAsideLayout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKCheckout;
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react'
|
||||
import { Card, CardBody } from 'reactstrap'
|
||||
import FalconCardHeader from '../common/FalconCardHeader'
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const JKCheckoutSuccess = () => {
|
||||
const {t} = useTranslation('checkoutSuccess')
|
||||
return (
|
||||
<Card>
|
||||
<FalconCardHeader title={t('page_title')} titleClass="font-weight-semi-bold" />
|
||||
<CardBody className="pt-0 text-center mt-4">
|
||||
<p className="text-muted">Thank you for your order! We'll send you an order confirmation email shortly.</p>
|
||||
<p>
|
||||
Click the button below to start using your new JamTracks.
|
||||
</p>
|
||||
<p>
|
||||
<Link to="/jamtracks/my" className="btn btn-primary">
|
||||
{t('my_jamtracks')}
|
||||
</Link>
|
||||
</p>
|
||||
<div>
|
||||
<p>
|
||||
You can also play with your JamTracks in the <a href="https://www.jamkazam.com/downloads" target='_blank'>JamKazam desktop app</a>, available for Windows and Mac.
|
||||
</p>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default JKCheckoutSuccess
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
import FalconCardHeader from '../common/FalconCardHeader';
|
||||
import { Button, Card, CardBody } from 'reactstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ButtonIcon from '../common/ButtonIcon';
|
||||
import ShoppingCartFooter from './shopping-cart/ShoppingCartFooter';
|
||||
import ShoppingCartTable from './shopping-cart/ShoppingCartTable';
|
||||
import { isIterableArray } from '../../helpers/utils';
|
||||
import classNames from 'classnames';
|
||||
import { useShoppingCart } from '../../hooks/useShoppingCart';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useResponsive } from '@farfetch/react-context-responsive';
|
||||
|
||||
const JKShoppingCart = () => {
|
||||
const { greaterThan } = useResponsive();
|
||||
const { shoppingCart, loading, removeCartItem } = useShoppingCart();
|
||||
|
||||
const handleRemoveItem = async id => {
|
||||
if (await removeCartItem(id)) {
|
||||
//show toast
|
||||
toast.success('Item removed from cart');
|
||||
}else{
|
||||
toast.error('Error removing item');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card style={{ width: greaterThan.sm ? '60%' : '100%' }} className="mx-auto">
|
||||
<FalconCardHeader title={`Shopping Cart (${shoppingCart.length} Items)`} light={false} breakPoint="sm">
|
||||
<ButtonIcon
|
||||
icon="chevron-left"
|
||||
color={classNames({
|
||||
'outline-secondary': isIterableArray(shoppingCart),
|
||||
primary: !isIterableArray(shoppingCart)
|
||||
})}
|
||||
size="sm"
|
||||
className={classNames({ 'border-300': !isIterableArray(shoppingCart) })}
|
||||
tag={Link}
|
||||
to="/jamtracks"
|
||||
>
|
||||
Continue Shopping
|
||||
</ButtonIcon>
|
||||
{isIterableArray(shoppingCart) && (
|
||||
<Button tag={Link} color="primary" size="sm" to="/checkout" className="ml-2">
|
||||
Checkout
|
||||
</Button>
|
||||
)}
|
||||
</FalconCardHeader>
|
||||
<CardBody className="p-0">
|
||||
<ShoppingCartTable shoppingCart={shoppingCart} loading={loading} onRemoveItem={handleRemoveItem} />
|
||||
</CardBody>
|
||||
{isIterableArray(shoppingCart) && <ShoppingCartFooter />}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKShoppingCart;
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import React, { Fragment, useContext } from 'react';
|
||||
//import PropTypes from 'prop-types';
|
||||
import AppContext from '../../../context/Context';
|
||||
import { Alert, Card, CardBody, CardFooter, Media, Table } from 'reactstrap';
|
||||
import FalconCardHeader from '../../common/FalconCardHeader';
|
||||
import {isIterableArray } from '../../../helpers/utils';
|
||||
import Flex from '../../common/Flex';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useShoppingCart } from '../../../hooks/useShoppingCart';
|
||||
|
||||
const CheckoutAside = () => {
|
||||
const { currency } = useContext(AppContext);
|
||||
const { shoppingCart, cartTotal } = useShoppingCart();
|
||||
return (
|
||||
<Card>
|
||||
<FalconCardHeader title="Order Summary" titleTag="h5" light={false}>
|
||||
{/* <ButtonIcon
|
||||
color="link"
|
||||
size="sm"
|
||||
tag={Link}
|
||||
icon="pencil-alt"
|
||||
className="btn-reveal text-600"
|
||||
to="/e-commerce/shopping-cart"
|
||||
/> */}
|
||||
</FalconCardHeader>
|
||||
{isIterableArray(shoppingCart) ? (
|
||||
<Fragment>
|
||||
<CardBody className="pt-0">
|
||||
<Table borderless className="fs--1 mb-0">
|
||||
<tbody>
|
||||
{shoppingCart.map((shoppingCartItem) => {
|
||||
return (
|
||||
<tr className="border-bottom" key={shoppingCartItem.id}>
|
||||
<th className="pl-0">
|
||||
{ shoppingCartItem.product_info.sale_display }
|
||||
</th>
|
||||
<th className="pr-0 text-right">
|
||||
{currency}
|
||||
{shoppingCartItem.product_info.total_price }
|
||||
</th>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{/* <tr className="border-bottom">
|
||||
<th className="pl-0">Subtotal</th>
|
||||
<th className="pr-0 text-right">
|
||||
{currency}
|
||||
{subTotal}
|
||||
</th>
|
||||
</tr> */}
|
||||
|
||||
{/* <tr className="border-bottom">
|
||||
<th className="pl-0">Shipping</th>
|
||||
<th className="pr-0 text-right text-nowrap">
|
||||
+ {currency}
|
||||
{calculatedShippingCost}
|
||||
</th>
|
||||
</tr> */}
|
||||
<tr>
|
||||
<th className="pl-0 pb-0">Total</th>
|
||||
<th className="pr-0 text-right pb-0 text-nowrap">
|
||||
{currency}
|
||||
{cartTotal}
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</Table>
|
||||
</CardBody>
|
||||
<CardFooter tag={Flex} justify="between" className="bg-100">
|
||||
<div className="font-weight-semi-bold">Payable Total</div>
|
||||
<div className="font-weight-bold">
|
||||
{currency}
|
||||
{cartTotal}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Fragment>
|
||||
) : (
|
||||
<CardBody className="p-0">
|
||||
<Alert color="warning" className="mb-0 rounded-0 overflow-hidden">
|
||||
<Media className="align-items-center">
|
||||
<FontAwesomeIcon icon={['far', 'dizzy']} className="fs-5" />
|
||||
<Media body className="ml-3">
|
||||
<p className="mb-0">You have no items in your shopping cart. Go ahead and start shopping!</p>
|
||||
</Media>
|
||||
</Media>
|
||||
</Alert>
|
||||
</CardBody>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// CheckoutAside.propTypes = {
|
||||
// };
|
||||
|
||||
export default CheckoutAside;
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
import React, { useState, useContext, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AppContext, { ProductContext } from '../../../context/Context';
|
||||
import { Button, Card, CardBody, Col, CustomInput, FormGroup, Media, Row, UncontrolledTooltip } from 'reactstrap';
|
||||
import FalconCardHeader from '../../common/FalconCardHeader';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import FalconInput from '../../common/FalconInput';
|
||||
import Flex from '../../common/Flex';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import iconPaymentMethodsGrid from '../../../assets/img/icons/icon-payment-methods-grid.png';
|
||||
import iconPaypalFull from '../../../assets/img/icons/icon-paypal-full.png';
|
||||
import shield from '../../../assets/img/icons/shield.png';
|
||||
|
||||
const CheckoutPaymentMethod = ({ payableTotal, paymentMethod, setPaymentMethod }) => {
|
||||
const { currency } = useContext(AppContext);
|
||||
const { shoppingCart, shoppingCartDispatch } = useContext(ProductContext);
|
||||
const [cardNumber, setCardNumber] = useState('');
|
||||
const [expDate, setExpDate] = useState('');
|
||||
const [cvv, setCvv] = useState('');
|
||||
|
||||
const handlePayment = () => {
|
||||
toast(
|
||||
<div className="text-700">
|
||||
<h5 className="text-success fs-0 mb-0">Payment success!</h5>
|
||||
<hr className="my-2" />
|
||||
Total:{' '}
|
||||
<strong>
|
||||
{currency}
|
||||
{payableTotal}
|
||||
</strong>
|
||||
<br />
|
||||
Payment method: <strong className="text-capitalize">{paymentMethod.split('-').join(' ')}</strong>
|
||||
</div>
|
||||
);
|
||||
shoppingCart.map(({ id }) => shoppingCartDispatch({ type: 'REMOVE', id }));
|
||||
};
|
||||
|
||||
const labelClassName = 'ls text-uppercase text-600 font-weight-semi-bold mb-0';
|
||||
return (
|
||||
<Card className="mb-3">
|
||||
<FalconCardHeader title="Payment Method" titleTag="h5" />
|
||||
<CardBody>
|
||||
<Row form>
|
||||
<Col xs={12}>
|
||||
<CustomInput
|
||||
label={
|
||||
<Flex align="center" className="mb-2 fs-1">
|
||||
Credit Card
|
||||
</Flex>
|
||||
}
|
||||
id="credit-card"
|
||||
value="credit-card"
|
||||
checked={paymentMethod === 'credit-card'}
|
||||
onChange={({ target }) => setPaymentMethod(target.value)}
|
||||
type="radio"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} className="pl-4">
|
||||
<Row>
|
||||
<Col sm={8}>
|
||||
<Row form className="align-items-center">
|
||||
<Col>
|
||||
<FormGroup>
|
||||
<FalconInput
|
||||
label="Card Number"
|
||||
labelClassName={labelClassName}
|
||||
className="input-spin-none"
|
||||
placeholder="•••• •••• •••• ••••"
|
||||
value={cardNumber}
|
||||
onChange={setCardNumber}
|
||||
type="number"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row form className="align-items-center">
|
||||
<Col xs={6}>
|
||||
<FormGroup>
|
||||
<FalconInput
|
||||
label="Exp Date"
|
||||
labelClassName={labelClassName}
|
||||
placeholder="mm/yyyy"
|
||||
value={expDate}
|
||||
onChange={setExpDate}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<FormGroup>
|
||||
<FalconInput
|
||||
label={
|
||||
<Fragment>
|
||||
CVV
|
||||
<span className="d-inline-block cursor-pointer text-primary" id="CVVTooltip">
|
||||
<FontAwesomeIcon icon="question-circle" className="mx-2" />
|
||||
</span>
|
||||
<UncontrolledTooltip placement="top" target="CVVTooltip">
|
||||
Card verification value
|
||||
</UncontrolledTooltip>
|
||||
</Fragment>
|
||||
}
|
||||
labelClassName={labelClassName}
|
||||
className="input-spin-none"
|
||||
placeholder="123"
|
||||
maxLength="3"
|
||||
pattern="[0-9]{3}"
|
||||
value={cvv}
|
||||
onChange={setCvv}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<div className="col-4 text-center pt-2 d-none d-sm-block">
|
||||
<div className="rounded p-2 mt-3 bg-100">
|
||||
<div className="text-uppercase fs--2 font-weight-bold">We Accept</div>
|
||||
<img src={iconPaymentMethodsGrid} alt="" width="120" />
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mt-3">
|
||||
<Col xs={12}>
|
||||
<CustomInput
|
||||
label={<img className="pull-right" src={iconPaypalFull} height="20" alt="" />}
|
||||
id="paypal"
|
||||
value="paypal"
|
||||
checked={paymentMethod === 'paypal'}
|
||||
onChange={({ target }) => setPaymentMethod(target.value)}
|
||||
type="radio"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr className="border-dashed my-5" />
|
||||
<Row>
|
||||
<Col md={7} xl={12} className="col-xxl-7 vertical-line px-md-3 mb-xxl-0">
|
||||
<Media>
|
||||
<img className="" src={shield} alt="" width="60" />
|
||||
<Media body className="ml-3">
|
||||
<h5 className="mb-2">Buyer Protection</h5>
|
||||
<CustomInput
|
||||
id="protection-option-1"
|
||||
label={
|
||||
<Fragment>
|
||||
<strong>Full Refund </strong>If you don't <br className="d-none d-md-block d-lg-none" />
|
||||
receive your order
|
||||
</Fragment>
|
||||
}
|
||||
type="checkbox"
|
||||
/>
|
||||
<CustomInput
|
||||
id="protection-option-2"
|
||||
label={
|
||||
<Fragment>
|
||||
<strong>Full or Partial Refund, </strong>If the product is not as described in details
|
||||
</Fragment>
|
||||
}
|
||||
type="checkbox"
|
||||
/>
|
||||
<Link className="fs--1 ml-3 pl-2" to="#!">
|
||||
Learn More
|
||||
<FontAwesomeIcon icon="caret-right" transform="down-2" className="ml-1" />
|
||||
</Link>
|
||||
</Media>
|
||||
</Media>
|
||||
</Col>
|
||||
<Col
|
||||
md={5}
|
||||
xl={12}
|
||||
className="col-xxl-5 pl-lg-4 pl-xl-2 pl-xxl-5 text-center text-md-left text-xl-center text-xxl-left"
|
||||
>
|
||||
<hr className="border-dashed d-block d-md-none d-xl-block d-xxl-none my-4" />
|
||||
<div className="fs-2 font-weight-semi-bold">
|
||||
All Total:{' '}
|
||||
<span className="text-primary">
|
||||
{currency}
|
||||
{payableTotal}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
color="success"
|
||||
className="mt-3 px-5"
|
||||
type="submit"
|
||||
disabled={!payableTotal}
|
||||
onClick={handlePayment}
|
||||
>
|
||||
Confirm & Pay
|
||||
</Button>
|
||||
<p className="fs--1 mt-3 mb-0">
|
||||
By clicking <strong>Confirm & Pay </strong>button you agree to the{' '}
|
||||
<Link to="#!">Terms & Conditions</Link>
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
CheckoutPaymentMethod.propTypes = {
|
||||
payableTotal: PropTypes.number.isRequired,
|
||||
paymentMethod: PropTypes.string.isRequired,
|
||||
setPaymentMethod: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CheckoutPaymentMethod;
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Card, CardBody, Col, Input, Label, Row } from 'reactstrap';
|
||||
import FalconCardHeader from '../../common/FalconCardHeader';
|
||||
import ButtonIcon from '../../common/ButtonIcon';
|
||||
|
||||
const CheckoutShippingAddress = ({ shippingAddress, setShippingAddress }) => (
|
||||
<Card className="mb-3">
|
||||
<FalconCardHeader title="Your Shipping Address" titleTag="h5">
|
||||
<ButtonIcon icon="plus" color="falcon-default" size="sm" transform="shrink-2">
|
||||
Add New Address
|
||||
</ButtonIcon>
|
||||
</FalconCardHeader>
|
||||
<CardBody>
|
||||
<Row>
|
||||
<Col md={6} className="mb-3 mb-md-0">
|
||||
<div className="custom-control custom-radio radio-select">
|
||||
<Input
|
||||
className="custom-control-input"
|
||||
id="address-1"
|
||||
type="radio"
|
||||
value="address-1"
|
||||
checked={shippingAddress === 'address-1'}
|
||||
onChange={({ target }) => setShippingAddress(target.value)}
|
||||
/>
|
||||
<Label className="custom-control-label font-weight-bold d-block" htmlFor="address-1">
|
||||
Antony Hopkins
|
||||
<span className="radio-select-content">
|
||||
<span>
|
||||
{' '}
|
||||
2392 Main Avenue,
|
||||
<br />
|
||||
Pensaukee,
|
||||
<br />
|
||||
New Jersey 02139<span className="d-block mb-0 pt-2">+(856) 929-229</span>
|
||||
</span>
|
||||
</span>
|
||||
</Label>
|
||||
<small className="text-primary cursor-pointer">Edit</small>
|
||||
</div>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<div className="position-relative">
|
||||
<div className="custom-control custom-radio radio-select">
|
||||
<Input
|
||||
className="custom-control-input"
|
||||
id="address-2"
|
||||
type="radio"
|
||||
value="address-2"
|
||||
checked={shippingAddress === 'address-2'}
|
||||
onChange={({ target }) => setShippingAddress(target.value)}
|
||||
/>
|
||||
<Label className="custom-control-label font-weight-bold d-block" htmlFor="address-2">
|
||||
Robert Bruce
|
||||
<span className="radio-select-content">
|
||||
<span>
|
||||
3448 Ile De France St #242,
|
||||
<br />
|
||||
Fort Wainwright, <br />
|
||||
Alaska, 99703<span className="d-block mb-0 pt-2">+(901) 637-734</span>
|
||||
</span>
|
||||
</span>
|
||||
</Label>
|
||||
<small className="text-primary cursor-pointer">Edit</small>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
CheckoutShippingAddress.propTypes = {
|
||||
shippingAddress: PropTypes.string.isRequired,
|
||||
setShippingAddress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CheckoutShippingAddress;
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Col, Media, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { calculateSale } from '../../../helpers/utils';
|
||||
import AppContext from '../../../context/Context';
|
||||
import classNames from 'classnames';
|
||||
import ButtonIcon from '../../common/ButtonIcon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
const CartModal = ({ type, id, quantity, title, files, price, sale, modal, setModal }) => {
|
||||
const { currency } = useContext(AppContext);
|
||||
if (!id) return null;
|
||||
return (
|
||||
<Modal isOpen={modal} toggle={() => setModal(!modal)} size="lg">
|
||||
<ModalHeader toggle={() => setModal(!modal)} className="border-200">
|
||||
<Media className="align-items-center">
|
||||
<div
|
||||
className={classNames('icon-item shadow-none', {
|
||||
'bg-soft-danger': type === 'REMOVE',
|
||||
'bg-soft-success': type === 'ADD'
|
||||
})}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={classNames({
|
||||
exclamation: type === 'REMOVE',
|
||||
'cart-plus': type === 'ADD'
|
||||
})}
|
||||
className={classNames({
|
||||
'text-warning': type === 'REMOVE',
|
||||
'text-success': type === 'ADD'
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Media body className="ml-2">
|
||||
You just {(type === 'REMOVE' && 'removed') || (type === 'ADD' && 'added')} {quantity} item
|
||||
{quantity === 1 ? '' : 's'}
|
||||
</Media>
|
||||
</Media>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
className={classNames({
|
||||
'mb-1': type === 'REMOVE'
|
||||
})}
|
||||
>
|
||||
<Row noGutters className="align-items-center">
|
||||
<Col>
|
||||
<Media className="align-items-center">
|
||||
<Link to={`/e-commerce/product-details/${id}`}>
|
||||
<img
|
||||
className="rounded mr-3 d-none d-md-block"
|
||||
src={files[0]['src'] || files[0]['base64']}
|
||||
alt=""
|
||||
width="80"
|
||||
/>
|
||||
</Link>
|
||||
<Media body>
|
||||
<h5 className="fs-0">
|
||||
<Link className="text-900" to={`/e-commerce/product-details/${id}`}>
|
||||
{title}
|
||||
</Link>
|
||||
</h5>
|
||||
</Media>
|
||||
</Media>
|
||||
</Col>
|
||||
<Col sm="auto" className="pl-sm-3 d-none d-sm-block">
|
||||
{currency}
|
||||
{calculateSale(price, sale) * quantity}
|
||||
</Col>
|
||||
</Row>
|
||||
</ModalBody>
|
||||
{type !== 'REMOVE' && (
|
||||
<ModalFooter className="border-200">
|
||||
<Button color="secondary" size="sm" tag={Link} to="/e-commerce/checkout" onClick={() => setModal(!modal)}>
|
||||
Checkout
|
||||
</Button>
|
||||
|
||||
<ButtonIcon
|
||||
tag={Link}
|
||||
to="/e-commerce/shopping-cart"
|
||||
color="primary"
|
||||
size="sm"
|
||||
className="ml-2"
|
||||
icon="chevron-right"
|
||||
iconAlign="right"
|
||||
onClick={() => setModal(!modal)}
|
||||
>
|
||||
Go to Cart
|
||||
</ButtonIcon>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
CartModal.propTypes = { value: PropTypes.any };
|
||||
|
||||
CartModal.defaultProps = { value: `CartModal` };
|
||||
|
||||
export default CartModal;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import Flex from '../../common/Flex';
|
||||
import { Button, CardFooter } from 'reactstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const ShoppingCartFooter = () => {
|
||||
|
||||
return (
|
||||
<CardFooter tag={Flex} justify="end" className="bg-light">
|
||||
<Button tag={Link} color="primary" size="sm" to="/checkout" className="ml-2">
|
||||
Checkout
|
||||
</Button>
|
||||
</CardFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShoppingCartFooter;
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AppContext from '../../../context/Context';
|
||||
import { Col, Media, Row } from 'reactstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const ShoppingCartItem = ({ shoppingCartItem, onRemoveItem }) => {
|
||||
const { currency } = useContext(AppContext);
|
||||
const { id } = shoppingCartItem;
|
||||
|
||||
const handleRemoveClick = async () => {
|
||||
onRemoveItem(id);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row noGutters className="align-items-center px-1 border-bottom border-200">
|
||||
<Col xs={8} className="py-3 px-2 px-md-3">
|
||||
<Media className="align-items-center">
|
||||
<Media body>
|
||||
<h5 className="fs-0">
|
||||
<Link className="text-900" to={`/jamtracks`}>
|
||||
{ shoppingCartItem.product_info.sale_display }
|
||||
</Link>
|
||||
</h5>
|
||||
<div
|
||||
className="fs--2 fs-md--1 text-danger cursor-pointer"
|
||||
onClick={handleRemoveClick}
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</Media>
|
||||
</Media>
|
||||
</Col>
|
||||
<Col xs={4} className="p-3">
|
||||
<Row className="align-items-center">
|
||||
<Col md={8} className="d-flex justify-content-end justify-content-md-center px-2 px-md-3 order-1 order-md-0">
|
||||
<div>
|
||||
{ shoppingCartItem.quantity }
|
||||
</div>
|
||||
</Col>
|
||||
<Col md={4} className="text-right pl-0 pr-2 pr-md-3 order-0 order-md-1 mb-2 mb-md-0 text-600">
|
||||
{/* {currency}
|
||||
{calculateSale(price, sale) * quantity} */}
|
||||
{currency}
|
||||
{ shoppingCartItem.product_info.total_price }
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
ShoppingCartItem.propTypes = {
|
||||
shoppingCartItem: PropTypes.object.isRequired,
|
||||
onRemoveItem: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ShoppingCartItem;
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import React, { Fragment, useContext } from 'react';
|
||||
import { isIterableArray } from '../../../helpers/utils';
|
||||
import { Col, Row } from 'reactstrap';
|
||||
import ShoppingCartItem from './ShoppingCartItem';
|
||||
import AppContext from '../../../context/Context';
|
||||
import { useShoppingCart } from '../../../hooks/useShoppingCart';
|
||||
|
||||
const ShoppingCartTable = ({ shoppingCart, loading, onRemoveItem }) => {
|
||||
const { currency } = useContext(AppContext);
|
||||
const { cartTotal, cartSubTotal, cartTax } = useShoppingCart();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{loading ? (
|
||||
<Row noGutters className="bg-200 text-900 px-1 fs--1 font-weight-semi-bold">
|
||||
<Col xs={9} md={8} className="p-2 px-md-3">
|
||||
Loading...
|
||||
</Col>
|
||||
</Row>
|
||||
) : isIterableArray(shoppingCart) ? (
|
||||
<Fragment>
|
||||
<Row noGutters className="bg-200 text-900 px-1 fs--1 font-weight-semi-bold">
|
||||
<Col xs={9} md={8} className="p-2 px-md-3">
|
||||
Name
|
||||
</Col>
|
||||
<Col xs={3} md={4} className="px-3">
|
||||
<Row>
|
||||
<Col md={8} className="py-2 d-none d-md-block text-center">
|
||||
Quantity
|
||||
</Col>
|
||||
<Col md={4} className="text-right p-2 px-md-3">
|
||||
Price
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{shoppingCart.map(shoppingCartItem => (
|
||||
<ShoppingCartItem shoppingCartItem={shoppingCartItem} key={shoppingCartItem.id} onRemoveItem={onRemoveItem} />
|
||||
))}
|
||||
<Row noGutters className="font-weight-bold px-1">
|
||||
<Col xs={9} md={8} className="py-2 px-md-3 text-right text-900">
|
||||
Total
|
||||
</Col>
|
||||
<Col className="px-3">
|
||||
<Row>
|
||||
<Col md={8} className="py-2 d-none d-md-block text-center">
|
||||
{shoppingCart.length} (items)
|
||||
</Col>
|
||||
<Col className="col-12 col-md-4 text-right py-2 pr-md-3 pl-0">
|
||||
{currency}
|
||||
{cartTotal}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Fragment>
|
||||
) : (
|
||||
<p className="p-card mb-0 bg-light">You have no items in your shopping cart. Go ahead and start shopping!</p>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShoppingCartTable;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import routes from '../routes';
|
||||
import { getAppFeatures } from '../helpers/rest';
|
||||
|
||||
const AppRoutesContext = React.createContext(null);
|
||||
|
||||
export const AppRoutesProvider = ({ children }) => {
|
||||
const [appRoutes, setAppRoutes] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (appRoutes.length === 0) {
|
||||
const env = process.env.REACT_APP_ENV;
|
||||
getAppFeatures(env)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(features => {
|
||||
if (features.length === 0) {
|
||||
setAppRoutes(routes);
|
||||
} else {
|
||||
const _routes = routes.filter(route => {
|
||||
if(route.children && route.children.length > 0) {
|
||||
route.children = route.children.filter(child => {
|
||||
return presentInNav(features, child);
|
||||
});
|
||||
}
|
||||
return presentInNav(features, route);
|
||||
});
|
||||
setAppRoutes(_routes);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const presentInNav = (features, route) => {
|
||||
return features.find(
|
||||
feature => route.to === feature.handle && feature.is_enabled && feature.feature_type === 'page' && feature.env === process.env.REACT_APP_ENV
|
||||
);
|
||||
};
|
||||
|
||||
return <AppRoutesContext.Provider value={{ appRoutes }}>{children}</AppRoutesContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAppRoutes = () => React.useContext(AppRoutesContext);
|
||||
|
|
@ -5,7 +5,7 @@ const AppContext = createContext(settings);
|
|||
|
||||
export const EmailContext = createContext({ emails: [] });
|
||||
|
||||
export const ProductContext = createContext({ products: [] });
|
||||
export const ProductContext = createContext({ products: [], shoppingCart: [] });
|
||||
|
||||
export const FeedContext = createContext({ feeds: [] });
|
||||
|
||||
|
|
|
|||
|
|
@ -35,8 +35,12 @@ import {
|
|||
faPaperPlane as farPaperPlane,
|
||||
faQuestionCircle as farQuestionCircle,
|
||||
faSmileBeam as farSmileBeam,
|
||||
faStar as farStar
|
||||
faStar as farStar,
|
||||
faMinus as farMinus,
|
||||
faBell as farBell,
|
||||
far,
|
||||
} from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
import {
|
||||
faAlignLeft,
|
||||
faAlignRight,
|
||||
|
|
@ -162,7 +166,11 @@ import {
|
|||
faVolumeUp,
|
||||
faSpinner,
|
||||
faPlayCircle,
|
||||
faPauseCircle
|
||||
faPauseCircle,
|
||||
faStopCircle,
|
||||
faInfoCircle,
|
||||
faDownload,
|
||||
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
//import { faAcousticGuitar } from "../icons";
|
||||
|
|
@ -294,6 +302,7 @@ library.add(
|
|||
faSpinner,
|
||||
faPlayCircle,
|
||||
faPauseCircle,
|
||||
faStopCircle,
|
||||
|
||||
// Brand
|
||||
faFacebook,
|
||||
|
|
@ -310,8 +319,11 @@ library.add(
|
|||
faYoutube,
|
||||
faVideo,
|
||||
faInfo,
|
||||
faInfoCircle,
|
||||
faPhone,
|
||||
faTrello,
|
||||
faMinus,
|
||||
faDownload,
|
||||
|
||||
// Regular
|
||||
farHeart,
|
||||
|
|
@ -335,6 +347,7 @@ library.add(
|
|||
farCircle,
|
||||
farCopy,
|
||||
farComment,
|
||||
|
||||
farBell,
|
||||
|
||||
//faAcousticGuitar,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,14 +25,14 @@ export const getPersonById = id => {
|
|||
);
|
||||
};
|
||||
|
||||
export const getUserDetails = options => {
|
||||
export const getUserDetail = options => {
|
||||
const { id, ...rest } = options;
|
||||
return new Promise((resolve, reject) =>
|
||||
apiFetch(`/users/${id}?${new URLSearchParams(rest)}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getPeople = ({ data, offset, limit } = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -67,16 +67,16 @@ export const getPeopleIndex = () => {
|
|||
export const getLobbyUsers = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/users/lobby`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const updateUser = (id, data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/users/${id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
apiFetch(`/users/${id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
|
|
@ -278,15 +278,15 @@ export const postUpdateAccountPassword = (userId, options) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const requestPasswordReset = (userId) => {
|
||||
export const requestPasswordReset = userId => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/users/${userId}/request_reset_password`, {
|
||||
method: 'POST',
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const postUserAppInteraction = (userId, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -297,56 +297,54 @@ export const postUserAppInteraction = (userId, options) => {
|
|||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export const getSubscription = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch('/recurly/get_subscription')
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const changeSubscription = (plan_code) => {
|
||||
const options = {plan_code}
|
||||
export const changeSubscription = plan_code => {
|
||||
const options = { plan_code };
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch('/recurly/change_subscription', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const getInvoiceHistory = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/recurly/invoice_history?${new URLSearchParams(options)}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createAffiliatePartner = (options) => {
|
||||
export const createAffiliatePartner = options => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch('/affiliate_partners', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getAffiliatePartnerData = (userId) => {
|
||||
export const getAffiliatePartnerData = userId => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/users/${userId}/affiliate_partner`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getAffiliateSignups = (options = {}) => {
|
||||
if (!options.per_page) {
|
||||
|
|
@ -357,18 +355,18 @@ export const getAffiliateSignups = (options = {}) => {
|
|||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/affiliate_partners/signups?${new URLSearchParams(options)}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getAffiliatePayments = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/affiliate_partners/payments`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const postAffiliatePartnerData = (userId, params) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -376,19 +374,28 @@ export const postAffiliatePartnerData = (userId, params) => {
|
|||
method: 'POST',
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const autocompleteJamTracks = (input, limit) => {
|
||||
const query = { match: input, limit }
|
||||
const query = { match: input, limit };
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/jamtracks/autocomplete?${new URLSearchParams(query)}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getPurchasedJamTracks = (options = {}) => {
|
||||
options = { ...options, show_purchased_only: true };
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/jamtracks/purchased?${new URLSearchParams(options)}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const getJamTrackArtists = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -396,7 +403,7 @@ export const getJamTrackArtists = (options = {}) => {
|
|||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getJamTracks = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -404,15 +411,134 @@ export const getJamTracks = (options = {}) => {
|
|||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getJamTrack = options => {
|
||||
const { id, ...rest } = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/jamtracks/${id}?${new URLSearchParams(rest)}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const addJamtrackToShoppingCart = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/shopping_carts/add_jamtrack?`, {
|
||||
apiFetch(`/shopping_carts/add_jamtrack`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getAppFeatures = (env) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/app_features?env=${env}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
export const getShoppingCart = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/shopping_carts`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const removeShoppingCart = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/shopping_carts`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(options)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const getBillingInfo = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/recurly/billing_info`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const createRecurlyAccount = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/recurly/create_account`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const placeOrder = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/recurly/place_order`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const postUserEvent = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/users/event/record`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const userOpenedJamTrackWebPlayer = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/users/progression/opened_jamtrack_web_player`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const markMixdownActive = options => {
|
||||
const { id, ...rest } = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/jamtracks/${id}/mixdowns/active`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(rest)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
export const createMixdown = options => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/mixdowns`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
export const deleteMixdown = id => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/mixdowns/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { Howl } from 'howler';
|
||||
|
||||
const useBrowserMedia = (jamTrack) => {
|
||||
const [audio, setAudio] = useState(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
const [playPosition, setPlayPosition] = useState(0);
|
||||
//const [error, setError] = useState(null);
|
||||
const preLoad = true;
|
||||
|
||||
const manageMixdownSync = () => {
|
||||
const activeMixdown = jamTrack.mixdowns.find(mixdown => mixdown.id === jamTrack.last_mixdown_id)
|
||||
|
||||
const activeStem = jamTrack.tracks.find(stem => stem.id === jamTrack.last_stem_id);
|
||||
|
||||
if ( activeStem ) {
|
||||
} else if ( activeMixdown ) {
|
||||
} else if (jamTrack) {
|
||||
const masterTrack = jamTrack.tracks.find(track => track.track_type === 'Master');
|
||||
if (masterTrack) {
|
||||
loadMedia([masterTrack.preview_mp3_url]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!jamTrack) {
|
||||
return;
|
||||
}
|
||||
manageMixdownSync();
|
||||
|
||||
}, [jamTrack]);
|
||||
|
||||
useEffect(() => {
|
||||
if (playing) {
|
||||
const interval = setInterval(() => {
|
||||
if (audio) {
|
||||
const position = audio.seek();
|
||||
if(position === audio) {
|
||||
setPlayPosition(0);
|
||||
}else{
|
||||
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [playing]);
|
||||
|
||||
const loadMedia = (urls) => {
|
||||
if (audio) {
|
||||
audio.unload();
|
||||
}
|
||||
setLoading(true);
|
||||
setAudio(new Howl({
|
||||
src: urls,
|
||||
autoplay: false,
|
||||
loop: false,
|
||||
volume: 1,
|
||||
preload: true,
|
||||
onstop: () => {
|
||||
console.log('Audio stopped');
|
||||
setLoading(false);
|
||||
setPlaying(false);
|
||||
setPaused(false);
|
||||
},
|
||||
onend: () => {
|
||||
alert('Audio ended')
|
||||
console.log('Audio ended');
|
||||
setLoading(false);
|
||||
setPlaying(false);
|
||||
setPaused(false);
|
||||
|
||||
},
|
||||
onload: () => {
|
||||
console.log('Audio loaded');
|
||||
setLoading(false);
|
||||
setLoaded(true);
|
||||
},
|
||||
onloaderror: () => {
|
||||
console.log('Audio load error');
|
||||
setLoading(false);
|
||||
setLoadError(true);
|
||||
},
|
||||
onpause: () => {
|
||||
console.log('Audio paused');
|
||||
setPaused(true);
|
||||
setPlaying(false);
|
||||
},
|
||||
onplay: () => {
|
||||
console.log('Audio playing');
|
||||
setPlaying(true);
|
||||
setPaused(false);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const play = () => {
|
||||
if (audio) {
|
||||
audio.play();
|
||||
}
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
if (audio) {
|
||||
try {
|
||||
audio.pause();
|
||||
audio.seek(0);
|
||||
} catch (error) {
|
||||
console.log('Error stopping audio', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
if (audio) {
|
||||
audio.pause();
|
||||
}
|
||||
}
|
||||
|
||||
return { play, stop, pause, loading, loaded, playing, paused, loadError, playPosition };
|
||||
};
|
||||
|
||||
export default useBrowserMedia;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// import { useCookies } from 'react-cookie';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import Cookies from 'universal-cookie';
|
||||
|
||||
export const useCheckout = () => {
|
||||
const cookieName = 'preserve_billing';
|
||||
// const [setCookie, removeCookie, cookies] = useCookies([cookieName]);
|
||||
const cookies = new Cookies(null, { path: '/' });
|
||||
|
||||
const setPreserveBillingInfo = () => {
|
||||
const date = new Date();
|
||||
const minutes = 10;
|
||||
date.setTime(date.getTime() + minutes * 60 * 1000);
|
||||
//expires if there is a cookie with the same name
|
||||
//removeCookie(cookieName, { path: '/' });
|
||||
cookies.remove(cookieName, { path: '/' });
|
||||
//set the new cookie
|
||||
//setCookie(cookieName, 'jam', { path: '/', expires: date });
|
||||
cookies.set(cookieName, 'jam', { path: '/', expires: date });
|
||||
};
|
||||
|
||||
const shouldPreserveBillingInfo = useMemo(() => {
|
||||
return cookies.get(cookieName) === 'jam';
|
||||
}, [cookies]);
|
||||
|
||||
const refreshPreserveBillingInfo = () => {
|
||||
if (shouldPreserveBillingInfo) {
|
||||
setPreserveBillingInfo();
|
||||
}
|
||||
}
|
||||
|
||||
const deletePreserveBillingInfo = () => {
|
||||
cookies.remove(cookieName, { path: '/' });
|
||||
if(cookies.get(cookieName)){
|
||||
console.log('after deleting the preserve billing cookie, it still exists!');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
setPreserveBillingInfo,
|
||||
shouldPreserveBillingInfo,
|
||||
refreshPreserveBillingInfo,
|
||||
deletePreserveBillingInfo
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { getJamTrack } from '../helpers/rest';
|
||||
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
||||
|
||||
const useJamTrackAudio = jamTrack => {
|
||||
const [audioUrls, setAudioUrls] = useState([]);
|
||||
const [jamTrackRecord, setJamTrackRecord] = useState(jamTrack);
|
||||
const fpPromise = FingerprintJS.load();
|
||||
|
||||
const loadJamTrack = async () => {
|
||||
//console.log('_JAMTRACK_ loading jam track');
|
||||
try {
|
||||
const resp = await getJamTrack({ id: jamTrack.id });
|
||||
const data = await resp.json();
|
||||
setJamTrackRecord(data);
|
||||
} catch (error) {
|
||||
console.log('Error when fetching jam track', error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateMedia = async () => {
|
||||
//console.log('_JAMTRACK_ updating media', jamTrackRecord);
|
||||
const activeMixdown = jamTrackRecord.mixdowns.find(mixdown => mixdown.id === jamTrackRecord.last_mixdown_id);
|
||||
const activeStem = jamTrackRecord.tracks.find(stem => stem.id === jamTrackRecord.last_stem_id);
|
||||
|
||||
|
||||
if (activeStem) {
|
||||
//console.log('_JAMTRACK_ this is a stem', activeStem);
|
||||
} else if (activeMixdown) {
|
||||
//console.log('_JAMTRACK_ this is a mixdown', activeMixdown);
|
||||
const fp = await fpPromise;
|
||||
const result = await fp.get();
|
||||
const audioUrl =
|
||||
process.env.REACT_APP_API_BASE_URL +
|
||||
`/mixdowns/${activeMixdown.id}/download.mp3?file_type=mp3&sample_rate=48&mark=${result.visitorId}`;
|
||||
console.log('mixdown audioUrl', audioUrl);
|
||||
setAudioUrls([audioUrl]);
|
||||
} else if (jamTrack) {
|
||||
const masterTrack = jamTrack.tracks.find(track => track.track_type === 'Master');
|
||||
//console.log('_JAMTRACK_ this is the master track', masterTrack);
|
||||
if (masterTrack) {
|
||||
setAudioUrls([masterTrack.preview_mp3_url]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!jamTrack) {
|
||||
// return;
|
||||
// }
|
||||
// loadJamTrack();
|
||||
// }, [jamTrack]);
|
||||
|
||||
useEffect(() => {
|
||||
if (jamTrackRecord) {
|
||||
updateMedia();
|
||||
}
|
||||
}, [jamTrackRecord]);
|
||||
|
||||
return { audioUrls, loadJamTrack };
|
||||
};
|
||||
|
||||
export default useJamTrackAudio;
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { getShoppingCart, addJamtrackToShoppingCart, removeShoppingCart } from "../helpers/rest"
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
|
||||
export const useShoppingCart = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [shoppingCart, setShoppingCart] = useState([]);
|
||||
const [error, setError] = useState(null);
|
||||
const TAX_RATE = 0.1;
|
||||
|
||||
useEffect(() => {
|
||||
getCartItems();
|
||||
}, []);
|
||||
|
||||
const cartTotal = useMemo(() => {
|
||||
//calculate total price
|
||||
const totalPrice = shoppingCart.reduce((acc, item) => acc + parseFloat(item.product_info.total_price), 0.00);
|
||||
return totalPrice;
|
||||
}, [shoppingCart]);
|
||||
|
||||
|
||||
|
||||
const getCartItems = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const resp = await getShoppingCart();
|
||||
const data = await resp.json();
|
||||
setShoppingCart(data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setError(error);
|
||||
}finally{
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const addCartItem = async (options) => {
|
||||
try {
|
||||
const resp = await addJamtrackToShoppingCart(options);
|
||||
const data = await resp.json();
|
||||
setShoppingCart([...shoppingCart, data]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const removeCartItem = async (id) => {
|
||||
try {
|
||||
await removeShoppingCart({id});
|
||||
setShoppingCart(shoppingCart.filter(item => item.id !== id));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return{
|
||||
shoppingCart, error, loading, removeCartItem, addCartItem, cartTotal
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ import profileEN from './locales/en/profile.json'
|
|||
import accountEN from './locales/en/account.json'
|
||||
import affiliateEN from './locales/en/affiliate.json'
|
||||
import jamTracksEn from './locales/en/jamtracks.json'
|
||||
import checkoutEN from './locales/en/checkout.json'
|
||||
import checkoutSuccessEN from './locales/en/checkout_success.json'
|
||||
|
||||
import commonTranslationsES from './locales/es/common.json'
|
||||
import homeTranslationsES from './locales/es/home.json'
|
||||
|
|
@ -24,6 +26,8 @@ import profileES from './locales/es/profile.json'
|
|||
import accountES from './locales/es/account.json'
|
||||
import affiliateES from './locales/es/affiliate.json'
|
||||
import jamTracksEs from './locales/es/jamtracks.json'
|
||||
import checkoutES from './locales/es/checkout.json'
|
||||
import checkoutSuccessES from './locales/es/checkout_success.json'
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
fallbackLng: 'en',
|
||||
|
|
@ -40,7 +44,9 @@ i18n.use(initReactI18next).init({
|
|||
account: accountEN,
|
||||
friends: friendsTranslationsEN,
|
||||
affiliate: affiliateEN,
|
||||
jamtracks: jamTracksEn
|
||||
jamtracks: jamTracksEn,
|
||||
checkout: checkoutEN,
|
||||
checkoutSuccess: checkoutSuccessEN
|
||||
},
|
||||
es: {
|
||||
common: commonTranslationsES,
|
||||
|
|
@ -53,7 +59,9 @@ i18n.use(initReactI18next).init({
|
|||
account: accountES,
|
||||
friends: friendsTranslationsES,
|
||||
affiliate: affiliateES,
|
||||
jamtracks: jamTracksEs
|
||||
jamtracks: jamTracksEs,
|
||||
checkout: checkoutES,
|
||||
checkoutSuccess: checkoutSuccessES
|
||||
}
|
||||
},
|
||||
//ns: ['translations'],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"page_title": "Thank You!",
|
||||
"my_jamtracks": "My JamTracks"
|
||||
}
|
||||
|
|
@ -5,5 +5,40 @@
|
|||
"title": "Search",
|
||||
"placeholder": "Search by artist, song, style, or keyword"
|
||||
}
|
||||
},
|
||||
"my": {
|
||||
"page_title": "My JamTracks",
|
||||
"empty": {
|
||||
"title": "You haven't purchased any JamTracks yet",
|
||||
"description": "Browse our selection of JamTracks and find the perfect track to jam along with."
|
||||
},
|
||||
"search_input": {
|
||||
"title": "Search",
|
||||
"placeholder": "Enter song or artist name"
|
||||
}
|
||||
},
|
||||
"jamtrack": {
|
||||
"player": {
|
||||
"title": "JamTrack Player",
|
||||
"play": "Play",
|
||||
"pause": "Pause",
|
||||
"master_mix": "Master Mix"
|
||||
},
|
||||
"my_mixes": {
|
||||
"title": "My Mixes",
|
||||
"description": "Create New Mix",
|
||||
"Mixes": "Mixes",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"create_mix": {
|
||||
"title": "Create a Mix",
|
||||
"description": "Create a new mix by adjusting the volume of each instrument.",
|
||||
"tracks": "Tracks",
|
||||
"mute": "Mute",
|
||||
"tempo": "Tempo",
|
||||
"pitch": "Pitch",
|
||||
"mix_name": "Mix Name",
|
||||
"create": "Create Mix"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
"genres": "Genres",
|
||||
"first_name": "First Name",
|
||||
"last_name": "Last Name",
|
||||
"change_photo": "Change Photo",
|
||||
"change_photo": "Change",
|
||||
"country": "Country",
|
||||
"state": "State/Province",
|
||||
"city": "City",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"page_title": "Thank You!"
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
"genres": "Géneros",
|
||||
"first_name": "First Name",
|
||||
"last_name": "Last Name",
|
||||
"change_photo": "Change Photo",
|
||||
"change_photo": "Change",
|
||||
"country": "Country",
|
||||
"state": "State/Province",
|
||||
"city": "City",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { NativeAppProvider } from '../context/NativeAppContext';
|
|||
|
||||
import { JKLobbyChatProvider } from '../components/sessions/JKLobbyChatContext';
|
||||
|
||||
import { AppRoutesProvider } from '../context/AppRoutesContext';
|
||||
|
||||
const DashboardLayout = ({ location }) => {
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
|
|
@ -16,13 +18,15 @@ const DashboardLayout = ({ location }) => {
|
|||
|
||||
return (
|
||||
<UserAuth path={location.pathname}>
|
||||
<BrowserQueryProvider>
|
||||
<NativeAppProvider>
|
||||
<JKLobbyChatProvider>
|
||||
<DashboardMain />
|
||||
</JKLobbyChatProvider>
|
||||
</NativeAppProvider>
|
||||
</BrowserQueryProvider>
|
||||
<AppRoutesProvider>
|
||||
<BrowserQueryProvider>
|
||||
<NativeAppProvider>
|
||||
<JKLobbyChatProvider>
|
||||
<DashboardMain />
|
||||
</JKLobbyChatProvider>
|
||||
</NativeAppProvider>
|
||||
</BrowserQueryProvider>
|
||||
</AppRoutesProvider>
|
||||
</UserAuth>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const jamTrackRoutes = {
|
|||
exact: true,
|
||||
icon: 'record-vinyl',
|
||||
children: [
|
||||
{ to: '/jamtracks/my', name: 'My JamTracks'},
|
||||
{ to: '/my-jamtracks', name: 'My JamTracks'},
|
||||
{ to: '/jamtracks', name: 'Find JamTracks'},
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { deleteMixdown, getJamTrack, createMixdown } from '../../helpers/rest';
|
||||
|
||||
const initialState = {
|
||||
jamTrack: {},
|
||||
jamTrackLoadingStatus: 'idle',
|
||||
mixdownsLoadingStatus: 'idle',
|
||||
deleteMixdownStatus: 'idle',
|
||||
newMixdownLoadingStatus: 'idle',
|
||||
tempMixdownLoadingStatus: 'idle',
|
||||
|
||||
error: null
|
||||
}
|
||||
|
||||
export const fetchJamTrack = createAsyncThunk('jamTracks/fetchJamTrack', async(options, thunkAPI) => {
|
||||
const response = await getJamTrack(options)
|
||||
return response.json();
|
||||
});
|
||||
|
||||
export const createMyMixdown = createAsyncThunk('jamTracks/createMixdown', async(options, thunkAPI) => {
|
||||
const response = await createMixdown(options)
|
||||
return response.json();
|
||||
});
|
||||
|
||||
export const removeMixdown = createAsyncThunk('jamTracks/removeMixdown', async(options, thunkAPI) => {
|
||||
console.log('removeMixdown', options);
|
||||
const { id } = options;
|
||||
const response = await deleteMixdown(id)
|
||||
return { id };
|
||||
});
|
||||
|
||||
export const jamTrackSlice = createSlice({
|
||||
name: 'jamTrack',
|
||||
initialState,
|
||||
reducers: {
|
||||
addMixdown: (state, action) => {
|
||||
const payload = action.payload;
|
||||
const jamTrack = state.jamTrack;
|
||||
if (jamTrack) {
|
||||
state.jamTrack.mixdowns = [...jamTrack.mixdowns, payload];
|
||||
state.tempMixdownLoadingStatus = 'succeeded';
|
||||
}
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder
|
||||
.addCase(fetchJamTrack.pending, (state, action) => {
|
||||
state.jamTrackLoadingStatus = 'loading'
|
||||
state.mixdownsLoadingStatus = 'loading'
|
||||
})
|
||||
.addCase(fetchJamTrack.fulfilled, (state, action) => {
|
||||
state.jamTrack = action.payload
|
||||
state.jamTrackLoadingStatus = 'succeeded'
|
||||
|
||||
if (action.payload.mixdowns) {
|
||||
state.mixdownsLoadingStatus = 'succeeded'
|
||||
}
|
||||
})
|
||||
.addCase(fetchJamTrack.rejected, (state, action) => {
|
||||
state.status = 'failed'
|
||||
state.jamTrackLoadingStatus = 'failed'
|
||||
state.mixdownsLoadingStatus = 'failed'
|
||||
state.error = action.error.message;
|
||||
})
|
||||
.addCase(createMyMixdown.pending, (state, action) => {
|
||||
state.newMixdownLoadingStatus = 'loading'
|
||||
state.mixdownsLoadingStatus = 'loading'
|
||||
})
|
||||
.addCase(createMyMixdown.fulfilled, (state, action) => {
|
||||
state.jamTrack.mixdowns = [...state.jamTrack.mixdowns, action.payload];
|
||||
state.newMixdownLoadingStatus = 'succeeded'
|
||||
state.mixdownsLoadingStatus = 'succeeded'
|
||||
state.tempMixdownLoadingStatus = 'idle'
|
||||
})
|
||||
.addCase(createMyMixdown.rejected, (state, action) => {
|
||||
state.error = action.error.message;
|
||||
state.newMixdownLoadingStatus = 'failed'
|
||||
state.tempMixdownLoadingStatus = 'idle'
|
||||
})
|
||||
.addCase(removeMixdown.pending, (state, action) => {
|
||||
state.mixdownsLoadingStatus = 'loading'
|
||||
state.deleteMixdownStatus = 'loading'
|
||||
})
|
||||
.addCase(removeMixdown.fulfilled, (state, action) => {
|
||||
console.log('mixdown removed', action.payload)
|
||||
const mixdowns = state.jamTrack.mixdowns.filter(mix => mix.id !== action.payload.id);
|
||||
state.jamTrack.mixdowns = mixdowns;
|
||||
state.mixdowns = mixdowns;
|
||||
state.mixdownsLoadingStatus = 'succeeded'
|
||||
state.deleteMixdownStatus = 'succeeded'
|
||||
})
|
||||
.addCase(removeMixdown.rejected, (state, action) => {
|
||||
state.error = action.error.message;
|
||||
state.mixdownsLoadingStatus = 'failed'
|
||||
state.deleteMixdownStatus = 'failed'
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
export const { addMixdown } = jamTrackSlice.actions;
|
||||
export default jamTrackSlice.reducer;
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { getPurchasedJamTracks } from '../../helpers/rest';
|
||||
|
||||
const initialState = {
|
||||
jamTracks: [],
|
||||
status: 'idle',
|
||||
error: null,
|
||||
next: null
|
||||
};
|
||||
|
||||
export const fetchMyJamTracks = createAsyncThunk('jamTracks/fetchMyJamTracks', async (options, thunkAPI) => {
|
||||
const response = await getPurchasedJamTracks(options);
|
||||
return response.json();
|
||||
});
|
||||
|
||||
export const myJamTracksSlice = createSlice({
|
||||
name: 'jamTracks',
|
||||
initialState,
|
||||
reducers: {
|
||||
addJamTrack: (state, action) => {
|
||||
state.jamTracks.push(action.payload);
|
||||
},
|
||||
// updateJamTrack: (state, action) => {
|
||||
// const { id, name } = action.payload;
|
||||
// const existingJamTrack = state.jamTracks.find(jamTrack => jamTrack.id === id);
|
||||
// if (existingJamTrack) {
|
||||
// existingJamTrack.name = name;
|
||||
// }
|
||||
// },
|
||||
deleteJamTrack: (state, action) => {
|
||||
const { id } = action.payload;
|
||||
state.jamTracks = state.jamTracks.filter(jamTrack => jamTrack.id !== id);
|
||||
},
|
||||
filterJamTracks: (state, action) => {
|
||||
state.jamTracks = state.jamTracks.filter(jamTrack =>
|
||||
jamTrack.name.toLowerCase().includes(action.payload.toLowerCase())
|
||||
);
|
||||
}
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder
|
||||
.addCase(fetchMyJamTracks.pending, (state, action) => {
|
||||
state.status = 'loading';
|
||||
})
|
||||
.addCase(fetchMyJamTracks.fulfilled, (state, action) => {
|
||||
const append = action.meta.arg.append;
|
||||
//--- amend the state to include only unique jamTracks
|
||||
if (append) {
|
||||
const records = new Set([...state.jamTracks, ...action.payload.jamtracks]);
|
||||
const unique = [];
|
||||
records.map(x => (unique.filter(a => a.id === x.id).length > 0 ? null : unique.push(x)));
|
||||
state.jamTracks = unique;
|
||||
state.next = action.payload.next;
|
||||
state.status = 'succeeded';
|
||||
} else {
|
||||
state.status = 'succeeded';
|
||||
state.jamTracks = action.payload.jamtracks;
|
||||
state.next = action.payload.next;
|
||||
}
|
||||
//---
|
||||
})
|
||||
.addCase(fetchMyJamTracks.rejected, (state, action) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.error.message;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const { addJamTrack, deleteJamTrack, filterJamTracks } = myJamTracksSlice.actions;
|
||||
export default myJamTracksSlice.reducer;
|
||||
|
|
@ -8,6 +8,8 @@ import notificationReducer from './features/notificationSlice'
|
|||
import latencyReducer from "./features/latencySlice"
|
||||
import friendReducer from "./features/friendsSlice"
|
||||
import sessionsHistoryReducer from "./features/sessionsHistorySlice"
|
||||
import myJamTracksSlice from "./features/myJamTracksSlice"
|
||||
import jamTrackSlice from "./features/jamTrackSlice"
|
||||
|
||||
export default configureStore({
|
||||
reducer: {
|
||||
|
|
@ -20,5 +22,7 @@ export default configureStore({
|
|||
lobbyChat: lobbyChatMessagesReducer,
|
||||
friend: friendReducer,
|
||||
sessionsHistory: sessionsHistoryReducer, // this is the slice that holds the sessions history
|
||||
myJamTrack: myJamTracksSlice,
|
||||
jamTrack: jamTrackSlice
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
class CreateAppFeatures < ActiveRecord::Migration
|
||||
def self.up
|
||||
execute(<<-SQL
|
||||
CREATE TABLE public.app_features (
|
||||
id character varying(64) DEFAULT public.uuid_generate_v4() PRIMARY KEY NOT NULL,
|
||||
feature_type character varying(64) NOT NULL,
|
||||
handle character varying(1024) NOT NULL,
|
||||
is_enabled boolean DEFAULT false NOT NULL,
|
||||
env character varying(16) DEFAULT 'development' NOT NULL
|
||||
);
|
||||
SQL
|
||||
)
|
||||
end
|
||||
def self.down
|
||||
execute("DROP TABLE public.app_features")
|
||||
end
|
||||
end
|
||||
|
|
@ -345,6 +345,7 @@ require "jam_ruby/models/ad_campaign"
|
|||
require "jam_ruby/models/user_asset"
|
||||
require "jam_ruby/models/user_match_email_sending"
|
||||
require "jam_ruby/models/app_interaction"
|
||||
require "jam_ruby/models/app_feature"
|
||||
|
||||
|
||||
include Jampb
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
class JamRuby::AppFeature < ActiveRecord::Base
|
||||
|
||||
FEATURE_TYPES = %w(page)
|
||||
|
||||
attr_accessible :feature_type, :handle, :is_enabled, :env, as: :admin
|
||||
|
||||
#self.table_name = 'app_features'
|
||||
|
||||
validates :feature_type, presence: true, inclusion: {in: FEATURE_TYPES}
|
||||
validates :handle, presence: true, length: {maximum: 255}
|
||||
validates :is_enabled, inclusion: {in: [true, false]}
|
||||
validates :env, presence: true, inclusion: {in: %w(production staging development)}
|
||||
end
|
||||
|
|
@ -308,21 +308,25 @@
|
|||
return;
|
||||
}
|
||||
if (options.mediaActions) {
|
||||
console.log("CASE 0", playbackMonitorMode)
|
||||
options.mediaActions.positionUpdate(playbackMonitorMode)
|
||||
}
|
||||
else {
|
||||
if (playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) {
|
||||
console.log("CASE 1")
|
||||
var positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs();
|
||||
var duration = context.jamClient.SessionGetJamTracksPlayDurationMs();
|
||||
var durationMs = duration.media_len;
|
||||
var isPlaying = context.jamClient.isSessionTrackPlaying();
|
||||
}
|
||||
else if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.BROWSER_MEDIA) {
|
||||
console.log("CASE 2")
|
||||
var positionMs = BrowserMediaStore.onGetPlayPosition() || 0
|
||||
var durationMs = BrowserMediaStore.onGetPlayDuration() || 0;
|
||||
var isPlaying = BrowserMediaStore.playing;
|
||||
}
|
||||
else {
|
||||
console.log("CASE 3")
|
||||
var positionMs = context.jamClient.SessionCurrrentPlayPosMs();
|
||||
var durationMs = context.jamClient.SessionGetTracksPlayDurationMs();
|
||||
var isPlaying = context.jamClient.isSessionTrackPlaying();
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ mixins.push(Reflux.listenTo(BrowserMediaPlaybackStore,"onMediaStateChanged"))
|
|||
tempos : [ 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208 ]
|
||||
|
||||
onMediaStateChanged: (changes) ->
|
||||
console.log("BrowserMediaControls onMediaStateChanged", changes)
|
||||
if changes.playbackStateChanged
|
||||
if @state.controls?
|
||||
if changes.playbackState == 'play_start'
|
||||
|
|
|
|||
|
|
@ -168,7 +168,6 @@ BrowserMediaActions = @BrowserMediaActions
|
|||
# let's check and see if we've asked the BrowserMediaStore to load this particular file or not
|
||||
|
||||
if @jamTrack?.activeStem
|
||||
|
||||
if @browserMediaState?.id != @jamTrack.activeStem.id
|
||||
new window.Fingerprint2().get((result, components) => (
|
||||
BrowserMediaActions.load(@jamTrack.activeStem.id, [window.location.protocol + '//' + window.location.host + "/api/jamtracks/#{@jamTrack.id}/stems/#{@jamTrack.activeStem.id}/download.mp3?file_type=mp3&mark=#{result}"], 'jamtrack_web_player')
|
||||
|
|
@ -184,7 +183,6 @@ BrowserMediaActions = @BrowserMediaActions
|
|||
|
||||
|
||||
else if @jamTrack?.activeMixdown
|
||||
|
||||
# if we don't have this on the server yet, don't engage the rest of this logic...
|
||||
return if @jamTrack.activeMixdown?.myPackage?.signing_state != 'SIGNED'
|
||||
|
||||
|
|
@ -205,7 +203,6 @@ BrowserMediaActions = @BrowserMediaActions
|
|||
@jamTrack.activeMixdown.client_state = 'downloading'
|
||||
|
||||
else if @jamTrack?
|
||||
|
||||
masterTrack = null
|
||||
for jamTrackTrack in @jamTrack.tracks
|
||||
if jamTrackTrack.track_type == 'Master'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
class ApiAppFeaturesController < ApiController
|
||||
before_filter :api_signed_in_user
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
env = params[:env] || 'development'
|
||||
@app_features = AppFeature.where(env: env)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
object @app_features
|
||||
extends "api_app_features/show"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
object @app_feature
|
||||
|
||||
attributes :id, :feature_type, :handle, :is_enabled, :env
|
||||
|
|
@ -860,5 +860,9 @@ Rails.application.routes.draw do
|
|||
match 'jamblasters/pairing/login' => 'api_jamblasters#login', :via => :post
|
||||
match 'jamblasters/pairing/store' => 'api_jamblasters#store_token', :via => :post
|
||||
match 'jamblasters/pairing/pair' => 'api_jamblasters#pair', :via => :post
|
||||
|
||||
|
||||
#app features
|
||||
match '/app_features' => 'api_app_features#index', :via => :get
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ApiAppFeaturesController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:user) { FactoryGirl.create(:user) }
|
||||
|
||||
before(:each) do
|
||||
controller.current_user = user
|
||||
end
|
||||
|
||||
describe "index" do
|
||||
it "returns app features of env" do
|
||||
FactoryGirl.create(:app_feature, env: 'production')
|
||||
get :index, env: 'production'
|
||||
response.should be_success
|
||||
json = JSON.parse(response.body, :symbolize_names => true)
|
||||
json.should have(1).item
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1125,4 +1125,12 @@ FactoryGirl.define do
|
|||
asset_type "image"
|
||||
filename "image.jpg"
|
||||
end
|
||||
|
||||
factory :app_feature, class: "JamRuby::AppFeature" do
|
||||
feature_type "page"
|
||||
handle "/"
|
||||
env "development"
|
||||
is_enabled true
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue