Merge branch 'develop' into feature/legacy-download

This commit is contained in:
Seth Call 2024-08-28 06:23:41 -05:00
commit 6833c68e40
98 changed files with 11352 additions and 675 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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');
});
});

View File

@ -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');
// });
});

View File

@ -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');
});
})
});

View File

@ -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')
});
})
});

View File

@ -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

View File

@ -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
}
]
}

View File

@ -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');
});

485
jam-ui/package-lock.json generated
View File

@ -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": {

View File

@ -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

View File

@ -7,6 +7,10 @@
margin-bottom: 1rem !important;
}
.form-control-is-invalid{
border-color: #dc3545;
}
/* -------------------------------------------------------------------------- */
/* Choices */

View File

@ -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%)
}

View File

@ -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}

View File

@ -1,9 +0,0 @@
import React from 'react'
const JKShoppingCart = () => {
return (
<div>ShoppingCart</div>
)
}
export default JKShoppingCart

View File

@ -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="" />;
}
}

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,9 @@
import React from 'react'
const JKJamTrackMyMixes = () => {
return (
<div>JKJamTrackMyMixes</div>
)
}
export default JKJamTrackMyMixes

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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>
);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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 {

View File

@ -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>
)}
</>
);
};

View File

@ -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">

View File

@ -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 />

View File

@ -16,7 +16,7 @@ const NavbarVerticalMenu = ({ routes, location }) => {
navs.push({ ...route, isOpened: true, index })
});
setNavRoutes(navs)
}, []);
}, [routes]);
const toggleOpened = (e, route) => {
e.preventDefault();

View File

@ -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>

View File

@ -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 = {

View File

@ -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={() => {}}>

View File

@ -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 &amp; 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 &amp; 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.&nbsp;{' '}
<strong>You will not be billed for any charges of any kind without your explicit authorization.</strong>
&nbsp; 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.&nbsp; </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 &amp; Pay
</Button>
<p className="fs--1 mt-3 mb-0">
By clicking <strong>Confirm &amp; 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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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 &amp; Pay
</Button>
<p className="fs--1 mt-3 mb-0">
By clicking <strong>Confirm &amp; Pay </strong>button you agree to the{' '}
<Link to="#!">Terms &amp; Conditions</Link>
</p>
</Col>
</Row>
</CardBody>
</Card>
);
};
CheckoutPaymentMethod.propTypes = {
payableTotal: PropTypes.number.isRequired,
paymentMethod: PropTypes.string.isRequired,
setPaymentMethod: PropTypes.func.isRequired
};
export default CheckoutPaymentMethod;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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: [] });

View File

@ -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,
);

View File

@ -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));
});
}

View File

@ -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;

View File

@ -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
}
};

View File

@ -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;

View File

@ -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
}
}

View File

@ -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'],

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,4 @@
{
"page_title": "Thank You!",
"my_jamtracks": "My JamTracks"
}

View File

@ -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"
}
}
}

View File

@ -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",

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,3 @@
{
"page_title": "Thank You!"
}

View File

@ -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",

View File

@ -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>
);
};

View File

@ -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'},
]
}

View File

@ -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;

View File

@ -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;

View File

@ -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
}
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -0,0 +1,2 @@
object @app_features
extends "api_app_features/show"

View File

@ -0,0 +1,3 @@
object @app_feature
attributes :id, :feature_type, :handle, :is_enabled, :env

View File

@ -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

View File

@ -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

View File

@ -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