jamtracks pages

includes jamtrack listing, my jamtracks, jamtrack details pages
This commit is contained in:
Nuwan 2024-06-21 18:16:29 +05:30
parent 106ea91361
commit 6e29efd307
26 changed files with 7901 additions and 343 deletions

View File

@ -23,55 +23,55 @@ describe('JamTracks Page', () => {
cy.get('input[type="search"]').should('exist');
});
describe.only('search artists', () => {
describe('search artists', () => {
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

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

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

View File

@ -49,8 +49,8 @@ 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 loadable from '@loadable/component';
@ -287,7 +287,8 @@ function JKDashboardMain() {
<PrivateRoute path="/affiliate/earnings" component={JKAffiliateEarnings} />
<PrivateRoute path="/affiliate/agreement" component={JKAffiliateAgreement} />
<PrivateRoute path="/jamtracks" component={JKJamTracksFilter} />
<PrivateRoute path="/cart" component={JKShoppingCart} />
<PrivateRoute path="/shopping-cart" component={JKShoppingCart} />
<PrivateRoute path="/checkout" component={JKCheckout} />
{/*Redirect*/}
<Redirect to="/errors/404" />
</Switch>

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

@ -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,11 @@ 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 [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();
@ -151,4 +150,24 @@ const JKJamTracksAutoComplete = ({ onSelect, onEnter, showDropdown, setShowDropd
);
};
JKJamTracksAutoComplete.propTypes = {
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

@ -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,24 +43,33 @@ const JKJamTracksFilter = () => {
options.song = selected.name;
}
if (jamTracksNextPage !== null) {
options.next = jamTracksNextPage;
}
return options;
};
const handleOnEnter = queryStr => {
const handleOnSelect = selected => {
setArtists([]);
setJamTracks([]);
setSelected(null);
setSearchTerm('');
setShowArtists(false);
setSelected(selected);
const params = queryOptions(selected);
fetchJamTracks(params);
};
const handleOnEnter = queryStr => {
setPage(1);
setArtists([]);
setJamTracks([]);
setSelected(x => null);
setSearchTerm(queryStr);
fetchArtists(queryStr);
const params = queryOptions(queryStr);
console.log('handleOnEnter _params_', params);
console.log('handleOnEnter _params', params, selected);
fetchJamTracks(params);
};
const handleOnSelectArtist = artist => {
setPage(1);
const selectedOpt = {
type: 'artist',
original_artist: artist.original_artist
@ -78,6 +79,12 @@ const JKJamTracksFilter = () => {
handleOnSelect(selectedOpt);
};
const handleOnNextJamTracksPage = () => {
const currentQuery = selected ? selected : searchTerm;
const params = queryOptions(currentQuery);
fetchJamTracks(params);
}
const fetchJamTracks = options => {
getJamTracks(options)
.then(resp => {
@ -85,8 +92,9 @@ const JKJamTracksFilter = () => {
})
.then(data => {
console.log('jamtracks', data);
setJamTracks(data.jamtracks);
setJamTracksNextPage(data.next);
setJamTracks(prev => [...prev, ...data.jamtracks]);
setNextOffset(data.next);
setPage(page => page + 1);
})
.catch(error => {
console.error('error', error);
@ -113,12 +121,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" />
@ -153,8 +155,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,523 @@
import React, { Fragment, useState, useContext, useEffect } 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 } from 'react-hook-form';
import FalconInput from '../common/FalconInput';
import { Link } 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, getUserDetails, getCountries } from '../../helpers/rest';
import { useAuth } from '../../context/UserAuth';
const JKCheckout = () => {
const { currency } = useContext(AppContext);
const { cartTotal: payableTotal, loading: cartLoading } = useShoppingCart();
const { greaterThan } = useResponsive();
const { currentUser } = useAuth();
const [paymentMethod, setPaymentMethod] = useState('credit-card');
const [reuseExistingCard, setReuseExistingCard] = useState(false);
const [billingInfo, setBillingInfo] = useState({});
const [countries, setCountries] = useState([]);
const labelClassName = 'ls text-600 font-weight-semi-bold mb-0';
const billingLabelClassName = ' text-right';
const {
control,
handleSubmit,
setValue,
formState: { errors }
} = useForm({
defaultValues: {
first_name: '',
last_name: '',
address1: '',
address2: '',
city: '',
state: '',
zip: '',
country: 'US',
number: '',
month: '',
year: '',
verification_value: ''
}
});
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
};
const userResp = await getUserDetails(options);
const userData = await userResp.json();
console.log('User Data:', userData);
setReuseExistingCard(userData.has_recurly_account && userData.reuse_card);
await populateBillingAddress();
};
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 clearBillingAddress = () => {
setValue('first_name', '');
setValue('last_name', '');
setValue('address1', '');
setValue('address2', '');
setValue('city', '');
setValue('state', '');
setValue('zip', '');
setValue('country', '');
};
const onSubmit = data => {
if (paymentMethod === 'credit-card') {
console.log('Credit Card Data:', data);
}
if (paymentMethod === 'paypal') {
console.log('Paypal Data:', data);
handoverToPaypal();
}
};
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}
>
<form onSubmit={handleSubmit(onSubmit)}>
<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.concat(billingLabelClassName)}>
First Name
</Label>
</Col>
<Col>
<Controller
name="first_name"
control={control}
rules={{ required: 'First Name is required' }}
render={({ field }) => <Input {...field} />}
/>
{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.concat(billingLabelClassName)}>
Last Name
</Label>
</Col>
<Col>
<Controller
name="last_name"
control={control}
rules={{ required: 'Last Name is required' }}
render={({ field }) => <Input {...field} />}
/>
{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.concat(billingLabelClassName)}>
Address 1
</Label>
</Col>
<Col>
<Controller
name="address1"
control={control}
rules={{ required: 'Address line 1 is required' }}
render={({ field }) => <Input {...field} />}
/>
{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.concat(billingLabelClassName)}>
Address 2
</Label>
</Col>
<Col>
<Controller name="address2" control={control} render={({ field }) => <Input {...field} />} />
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-right">
<Label for="city" className={labelClassName.concat(billingLabelClassName)}>
City
</Label>
</Col>
<Col>
<Controller
name="city"
control={control}
rules={{ required: 'City is required' }}
render={({ field }) => <Input {...field} />}
/>
{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.concat(billingLabelClassName)}>
State or Region
</Label>
</Col>
<Col>
<Controller
name="state"
control={control}
rules={{ required: 'State is required' }}
render={({ field }) => <Input {...field} />}
/>
{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.concat(billingLabelClassName)}>
Zip or Postal Code
</Label>
</Col>
<Col>
<Controller
name="zip"
control={control}
rules={{ required: 'Zip code is required' }}
render={({ field }) => <Input {...field} />}
/>
{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.concat(billingLabelClassName)}>
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>
<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 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>
<Controller
name="number"
control={control}
rules={{
required: 'Card number is required',
pattern: {
value: /^(\d{4} ){3}\d{4}$/i,
message: 'Card number must be 16 digits'
}
}}
render={({ field }) => (
<FalconInput
{...field}
label="Card Number"
labelclassName={labelClassName.concat(billingLabelClassName)}
className="input-spin-none"
placeholder="•••• •••• •••• ••••"
type="number"
/>
)}
/>
{errors.number && (
<div className="text-danger">
<small>{errors.number.message}</small>
</div>
)}
</FormGroup>
</Col>
</Row>
<Row form className="align-items-center">
<Col xs={4}>
<FormGroup>
<Controller
name="month"
control={control}
render={({ field }) => (
<FalconInput
{...field}
label="Exp Month"
labelclassName={labelClassName.concat(billingLabelClassName)}
placeholder="mm"
maxLength="2"
/>
)}
/>
</FormGroup>
</Col>
<Col xs={4}>
<FormGroup>
<Controller
name="year"
control={control}
render={({ field }) => (
<FalconInput
{...field}
label="Exp Year"
labelclassName={labelClassName.concat(billingLabelClassName)}
placeholder="yy"
maxLength="2"
/>
)}
/>
</FormGroup>
</Col>
<Col xs={4}>
<FormGroup>
<Controller
name="verification_value"
control={control}
render={({ field }) => (
<FalconInput
{...field}
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.concat(billingLabelClassName)}
className="input-spin-none"
placeholder="123"
maxLength="3"
pattern="[0-9]{3}"
/>
)}
/>
</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 color="primary" className="mt-3 px-5" type="submit" disabled={!payableTotal}>
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,55 @@
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';
const JKShoppingCart = () => {
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>
<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,63 @@
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';
const ShoppingCartTable = ({ shoppingCart, loading, onRemoveItem }) => {
const { currency } = useContext(AppContext);
const totalCartPrice = shoppingCart.reduce((acc, item) => acc + parseFloat(item.product_info.total_price), 0);
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}
{totalCartPrice}
</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

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

@ -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,27 @@ 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 = {}) => {
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 +402,7 @@ export const getJamTrackArtists = (options = {}) => {
.then(response => resolve(response))
.catch(error => reject(error));
});
}
};
export const getJamTracks = (options = {}) => {
return new Promise((resolve, reject) => {
@ -404,19 +410,108 @@ 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 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(`/api/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 getAppFeatures = (env) => {
return new Promise((resolve, reject) => {

View File

@ -0,0 +1,60 @@
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);
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
}
}