WIP: jamtracks, shopping cart and checkout pages
This commit is contained in:
parent
adafcb8569
commit
a3c511d2b0
|
|
@ -7224,6 +7224,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",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,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",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -7,6 +7,10 @@
|
|||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.form-control-is-invalid{
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Choices */
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ import JKAffiliateAgreement from '../affiliate/JKAffiliateAgreement';
|
|||
import JKJamTracksFilter from '../jamtracks/JKJamTracksFilter';
|
||||
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';
|
||||
|
|
@ -286,8 +289,11 @@ 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="/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" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import React, { useEffect, useState } 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, Progress } from 'reactstrap';
|
||||
import FalconCardHeader from '../common/FalconCardHeader';
|
||||
import { getJamTrack, getUserDetail, postUserEvent, userOpenedJamTrackWebPlayer } from '../../helpers/rest';
|
||||
import JKJamTrackPlayer from './JKJamTrackPlayer';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
|
||||
const JKJamTrack = () => {
|
||||
const { t } = useTranslation('jamtracks');
|
||||
const { greaterThan } = useResponsive();
|
||||
const { id } = useParams();
|
||||
const [jamTrack, setJamTrack] = useState(null);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { currentUser } = useAuth();
|
||||
|
||||
const fetchJamTrack = async () => {
|
||||
console.log('fetching jam track', id);
|
||||
try {
|
||||
setLoading(true);
|
||||
const resp = await getJamTrack({ id });
|
||||
const data = await resp.json();
|
||||
setJamTrack(data);
|
||||
} catch (error) {
|
||||
console.log('Error when fetching jam track', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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(() => {
|
||||
fetchJamTrack();
|
||||
}, [id]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col>
|
||||
<Card className="mx-auto mb-4">
|
||||
<FalconCardHeader title={t('jamtrack.player.title')} titleClass="font-weight-semi-bold" />
|
||||
<CardBody className="pt-3">
|
||||
<JKJamTrackPlayer jamTrack={jamTrack} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card className="mx-auto">
|
||||
<FalconCardHeader title={t('jamtrack.my_mixes.title')} titleClass="font-weight-semi-bold" />
|
||||
<CardBody className="pt-3" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col>
|
||||
<Card className="mx-auto">
|
||||
<FalconCardHeader title={t('jamtrack.create_mix.title')} titleClass="font-weight-semi-bold" />
|
||||
<CardBody className="pt-3" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col />
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKJamTrack;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react'
|
||||
|
||||
const JKJamTrackMyMixes = () => {
|
||||
return (
|
||||
<div>JKJamTrackMyMixes</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JKJamTrackMyMixes
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import React, { useState, useEffect } 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';
|
||||
|
||||
const JKJamTrackPlayer = ({ jamTrack }) => {
|
||||
const [mixes, setMixes] = useState([]);
|
||||
const [selectedMix, setSelectedMix] = useState(null);
|
||||
const { play, stop, pause, loading, loaded, playing, paused, loadError } = 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]);
|
||||
|
||||
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>
|
||||
<Row className="mt-3">
|
||||
<Col>
|
||||
<Progress color="secondary" value={0} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<div className="d-flex justify-content-between">
|
||||
<span>0:00</span>
|
||||
<span>3:00</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
JKJamTrackPlayer.propTypes = {
|
||||
jamTrack: PropTypes.object.isRequired,
|
||||
|
||||
};
|
||||
|
||||
export default JKJamTrackPlayer;
|
||||
|
|
@ -5,7 +5,16 @@ 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 [loading, setLoading] = useState(false);
|
||||
|
|
@ -17,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,6 +171,7 @@ const JKJamTracksAutoComplete = ({ onSelect, onEnter, showDropdown, setShowDropd
|
|||
};
|
||||
|
||||
JKJamTracksAutoComplete.propTypes = {
|
||||
fetchFunc: PropTypes.func.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onEnter: PropTypes.func.isRequired,
|
||||
showDropdown: PropTypes.bool.isRequired,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
@ -46,17 +46,18 @@ const JKJamTracksFilter = () => {
|
|||
return options;
|
||||
};
|
||||
|
||||
const handleOnSelect = selected => {
|
||||
const handleOnSelect = async (selected) => {
|
||||
setPage(1);
|
||||
setArtists([]);
|
||||
setJamTracks([]);
|
||||
setSearchTerm('');
|
||||
setShowArtists(false);
|
||||
setSelected(selected);
|
||||
const params = queryOptions(selected);
|
||||
fetchJamTracks(params);
|
||||
await fetchJamTracks(params);
|
||||
};
|
||||
|
||||
const handleOnEnter = queryStr => {
|
||||
const handleOnEnter = async(queryStr) => {
|
||||
setPage(1);
|
||||
setArtists([]);
|
||||
setJamTracks([]);
|
||||
|
|
@ -65,10 +66,10 @@ const JKJamTracksFilter = () => {
|
|||
fetchArtists(queryStr);
|
||||
const params = queryOptions(queryStr);
|
||||
console.log('handleOnEnter _params', params, selected);
|
||||
fetchJamTracks(params);
|
||||
await fetchJamTracks(params);
|
||||
};
|
||||
|
||||
const handleOnSelectArtist = artist => {
|
||||
const handleOnSelectArtist = async(artist) => {
|
||||
setPage(1);
|
||||
const selectedOpt = {
|
||||
type: 'artist',
|
||||
|
|
@ -76,30 +77,46 @@ const JKJamTracksFilter = () => {
|
|||
};
|
||||
setShowDropdown(false);
|
||||
setAutoCompleteInputValue('');
|
||||
handleOnSelect(selectedOpt);
|
||||
await handleOnSelect(selectedOpt);
|
||||
};
|
||||
|
||||
const handleOnNextJamTracksPage = () => {
|
||||
const handleOnNextJamTracksPage = async () => {
|
||||
const currentQuery = selected ? selected : searchTerm;
|
||||
const params = queryOptions(currentQuery);
|
||||
fetchJamTracks(params);
|
||||
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 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 fetchArtists = query => {
|
||||
const options = {
|
||||
|
|
@ -128,6 +145,7 @@ const JKJamTracksFilter = () => {
|
|||
<Row>
|
||||
<Col>
|
||||
<JKJamTracksAutoComplete
|
||||
fetchFunc={autocompleteJamTracks}
|
||||
onSelect={handleOnSelect}
|
||||
onEnter={handleOnEnter}
|
||||
showDropdown={showDropdown}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
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 { getPurchasedJamTracks } from '../../helpers/rest';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import useOnScreen from '../../hooks/useOnScreen';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const JKMyJamTracks = () => {
|
||||
const { t } = useTranslation('jamtracks');
|
||||
const { greaterThan } = useResponsive();
|
||||
const [jamTracks, setJamTracks] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const inputRef = React.createRef();
|
||||
const containerRef = useRef(null);
|
||||
const [lastJamTrackRef, setLastJamTrackRef] = useState(null);
|
||||
const isIntersecting = useOnScreen({ current: lastJamTrackRef });
|
||||
const [nextPage, setNextPage] = useState(1);
|
||||
|
||||
const handleInputChange = e => {
|
||||
const val = e.target.value;
|
||||
setInputValue(val);
|
||||
// const params = { page: 1, search: val };
|
||||
// fetchJamTracks(params);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getMyJamTracks = setTimeout(() => {
|
||||
fetchJamTracks({ page: 1, search: inputValue});
|
||||
}, 1000);
|
||||
return () => clearTimeout(getMyJamTracks);
|
||||
}, [inputValue]);
|
||||
|
||||
const fetchJamTracks = async (params) => {
|
||||
const { page } = params;
|
||||
try {
|
||||
setLoading(true);
|
||||
const resp = await getPurchasedJamTracks(params);
|
||||
const data = await resp.json();
|
||||
if (page === 1) {
|
||||
setJamTracks(data.jamtracks);
|
||||
}else{
|
||||
setJamTracks(prev => [...prev, ...data.jamtracks]);
|
||||
}
|
||||
setNextPage(data.next);
|
||||
|
||||
} catch (error) {
|
||||
console.log('Error when fetching jam tracks', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isIntersecting) {
|
||||
if (nextPage && !loading && nextPage !== 1) {
|
||||
const params = { page: nextPage };
|
||||
fetchJamTracks(params);
|
||||
}
|
||||
}
|
||||
}, [isIntersecting]);
|
||||
|
||||
useEffect(() => {
|
||||
const params = { page: nextPage };
|
||||
fetchJamTracks(params);
|
||||
}, []);
|
||||
|
||||
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' }}>
|
||||
{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">
|
||||
{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>
|
||||
{ loading && <div className="d-flex justify-content-center"> Loading... </div>}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKMyJamTracks;
|
||||
|
|
@ -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);
|
||||
// });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Fragment, useState, useContext, useEffect } from 'react';
|
||||
import React, { useState, useContext, useEffect, useMemo } from 'react';
|
||||
import ContentWithAsideLayout from '../../layouts/ContentWithAsideLayout';
|
||||
import AppContext from '../../context/Context';
|
||||
import CheckoutAside from './checkout/CheckoutAside';
|
||||
|
|
@ -16,36 +16,50 @@ import {
|
|||
} from 'reactstrap';
|
||||
import Select from 'react-select';
|
||||
import FalconCardHeader from '../common/FalconCardHeader';
|
||||
import { useForm, Controller, get } from 'react-hook-form';
|
||||
import { useForm, Controller, get, set } from 'react-hook-form';
|
||||
import FalconInput from '../common/FalconInput';
|
||||
import { Link } from 'react-router-dom';
|
||||
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, getUserDetails, getCountries } from '../../helpers/rest';
|
||||
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 [reuseExistingCard, setReuseExistingCard] = useState(false);
|
||||
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 billingLabelClassName = ' text-right';
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
setError,
|
||||
formState: { errors }
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
|
|
@ -64,6 +78,16 @@ const JKCheckout = () => {
|
|||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldPreserveBillingInfo) {
|
||||
refreshPreserveBillingInfo();
|
||||
setHasAlreadyEnteredBillingInfo(true);
|
||||
setHideBillingInfo(true);
|
||||
} else {
|
||||
setHideBillingInfo(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser) {
|
||||
fetchCountries();
|
||||
|
|
@ -88,11 +112,28 @@ const JKCheckout = () => {
|
|||
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();
|
||||
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 () => {
|
||||
|
|
@ -116,27 +157,127 @@ const JKCheckout = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const clearBillingAddress = () => {
|
||||
setValue('first_name', '');
|
||||
setValue('last_name', '');
|
||||
setValue('address1', '');
|
||||
setValue('address2', '');
|
||||
setValue('city', '');
|
||||
setValue('state', '');
|
||||
setValue('zip', '');
|
||||
setValue('country', '');
|
||||
};
|
||||
const disableCardFields = useMemo(() => {
|
||||
return paymentMethod === 'existing-card';
|
||||
}, [paymentMethod]);
|
||||
|
||||
const onSubmit = data => {
|
||||
if (paymentMethod === 'credit-card') {
|
||||
console.log('Credit Card Data:', data);
|
||||
}
|
||||
if (paymentMethod === 'paypal') {
|
||||
console.log('Paypal Data:', data);
|
||||
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`;
|
||||
|
|
@ -152,23 +293,53 @@ const JKCheckout = () => {
|
|||
aside={cartLoading ? <div>Cart Loading...</div> : <CheckoutAside />}
|
||||
isStickyAside={false}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{hasAlreadyEnteredBillingInfo && (
|
||||
<div className="alert alert-info" role="alert">
|
||||
<div className="d-flex">
|
||||
<FontAwesomeIcon icon="info-circle" className="mr-2" />
|
||||
<p>
|
||||
You recently entered payment info successfully. If you want to change your payment info, click the
|
||||
CHANGE PAYMENT INFO button. Otherwise, click the Confirm & Pay button to checkout.
|
||||
</p>
|
||||
</div>
|
||||
<div className='d-flex'>
|
||||
<Button onClick={() => setHideBillingInfo(!hideBillingInfo)}>
|
||||
{hideBillingInfo ? 'Change Payment Info' : 'Hide Payment Info'}
|
||||
</Button>
|
||||
<Button onClick={ doPlaceOrder } className="ml-2">
|
||||
Confirm & Pay
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={hideBillingInfo ? 'd-none' : 'd-block'}>
|
||||
{hasRedeemableJamTrack ? (
|
||||
<div className="alert alert-info d-flex" role="alert">
|
||||
<FontAwesomeIcon icon="info-circle" className="mr-2" />
|
||||
<p>
|
||||
Please enter your billing address and payment information below. {' '}
|
||||
<strong>You will not be billed for any charges of any kind without your explicit authorization.</strong>
|
||||
There are no "hidden" charges or fees, thank you!
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="alert alert-info d-flex" role="alert">
|
||||
<FontAwesomeIcon icon="info-circle" className="mr-2" />
|
||||
<p>Please enter your billing address and payment information below. </p>
|
||||
</div>
|
||||
)}
|
||||
<Card className="mb-3">
|
||||
<FalconCardHeader title="Billing Address" titleTag="h5" />
|
||||
<CardBody>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="first_name" className={labelClassName.concat(billingLabelClassName)}>
|
||||
<Label for="first_name" className={labelClassName}>
|
||||
First Name
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Controller
|
||||
name="first_name"
|
||||
control={control}
|
||||
rules={{ required: 'First Name is required' }}
|
||||
render={({ field }) => <Input {...field} />}
|
||||
/>
|
||||
<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>
|
||||
|
|
@ -178,17 +349,12 @@ const JKCheckout = () => {
|
|||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="last_name" className={labelClassName.concat(billingLabelClassName)}>
|
||||
<Label for="last_name" className={labelClassName}>
|
||||
Last Name
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Controller
|
||||
name="last_name"
|
||||
control={control}
|
||||
rules={{ required: 'Last Name is required' }}
|
||||
render={({ field }) => <Input {...field} />}
|
||||
/>
|
||||
<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>
|
||||
|
|
@ -198,17 +364,12 @@ const JKCheckout = () => {
|
|||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="address1" className={labelClassName.concat(billingLabelClassName)}>
|
||||
<Label for="address1" className={labelClassName}>
|
||||
Address 1
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Controller
|
||||
name="address1"
|
||||
control={control}
|
||||
rules={{ required: 'Address line 1 is required' }}
|
||||
render={({ field }) => <Input {...field} />}
|
||||
/>
|
||||
<input {...register('address1', { required: 'Address is required' })} className="form-control" />
|
||||
{errors.address1 && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.address1.message}</small>
|
||||
|
|
@ -218,27 +379,27 @@ const JKCheckout = () => {
|
|||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="address2" className={labelClassName.concat(billingLabelClassName)}>
|
||||
<Label for="address2" className={labelClassName}>
|
||||
Address 2
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Controller name="address2" control={control} render={({ field }) => <Input {...field} />} />
|
||||
<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.concat(billingLabelClassName)}>
|
||||
<Label for="city" className={labelClassName}>
|
||||
City
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Controller
|
||||
name="city"
|
||||
control={control}
|
||||
rules={{ required: 'City is required' }}
|
||||
render={({ field }) => <Input {...field} />}
|
||||
/>
|
||||
<input {...register('city', { required: 'City is required' })} className="form-control" />
|
||||
{errors.city && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.city.message}</small>
|
||||
|
|
@ -248,17 +409,12 @@ const JKCheckout = () => {
|
|||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="state" className={labelClassName.concat(billingLabelClassName)}>
|
||||
<Label for="state" className={labelClassName}>
|
||||
State or Region
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Controller
|
||||
name="state"
|
||||
control={control}
|
||||
rules={{ required: 'State is required' }}
|
||||
render={({ field }) => <Input {...field} />}
|
||||
/>
|
||||
<input {...register('state', { required: 'State or Region is required' })} className="form-control" />
|
||||
{errors.state && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.state.message}</small>
|
||||
|
|
@ -268,16 +424,14 @@ const JKCheckout = () => {
|
|||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="zip" className={labelClassName.concat(billingLabelClassName)}>
|
||||
<Label for="zip" className={labelClassName}>
|
||||
Zip or Postal Code
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Controller
|
||||
name="zip"
|
||||
control={control}
|
||||
rules={{ required: 'Zip code is required' }}
|
||||
render={({ field }) => <Input {...field} />}
|
||||
<input
|
||||
{...register('zip', { required: 'Zip or Postal Code is required' })}
|
||||
className="form-control"
|
||||
/>
|
||||
{errors.zip && (
|
||||
<div className="text-danger">
|
||||
|
|
@ -288,7 +442,7 @@ const JKCheckout = () => {
|
|||
</Row>
|
||||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-right">
|
||||
<Label for="country" className={labelClassName.concat(billingLabelClassName)}>
|
||||
<Label for="country" className={labelClassName}>
|
||||
Country
|
||||
</Label>
|
||||
</Col>
|
||||
|
|
@ -334,27 +488,36 @@ const JKCheckout = () => {
|
|||
<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>
|
||||
{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={
|
||||
|
|
@ -372,103 +535,82 @@ const JKCheckout = () => {
|
|||
<Col xs={12} className="pl-4">
|
||||
<Row>
|
||||
<Col sm={8}>
|
||||
<Row form className="align-items-center">
|
||||
<Row 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"
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={cardNumber}
|
||||
className={errors.number ? 'form-control form-control-is-invalid' : 'form-control'}
|
||||
placeholder="•••• •••• •••• ••••"
|
||||
onChange={handleOnCardNumberChange}
|
||||
disabled={disableCardFields}
|
||||
/>
|
||||
{errors.number && (
|
||||
{/* {errors.number && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.number.message}</small>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row form className="align-items-center">
|
||||
<Row 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"
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
<Controller
|
||||
name="year"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FalconInput
|
||||
{...field}
|
||||
label="Exp Year"
|
||||
labelclassName={labelClassName.concat(billingLabelClassName)}
|
||||
placeholder="yy"
|
||||
maxLength="2"
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
<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}"
|
||||
/>
|
||||
)}
|
||||
<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">
|
||||
|
|
@ -503,7 +645,7 @@ const JKCheckout = () => {
|
|||
{payableTotal}
|
||||
</span>
|
||||
</div>
|
||||
<Button color="primary" className="mt-3 px-5" type="submit" disabled={!payableTotal}>
|
||||
<Button type="submit" color="primary" className="mt-3 px-5" disabled={!payableTotal || submitting}>
|
||||
Confirm & Pay
|
||||
</Button>
|
||||
<p className="fs--1 mt-3 mb-0">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -9,8 +9,10 @@ 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 => {
|
||||
|
|
@ -23,7 +25,7 @@ const JKShoppingCart = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<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"
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ 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 totalCartPrice = shoppingCart.reduce((acc, item) => acc + parseFloat(item.product_info.total_price), 0);
|
||||
const { cartTotal, cartSubTotal, cartTax } = useShoppingCart();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
|
@ -47,7 +48,7 @@ const ShoppingCartTable = ({ shoppingCart, loading, onRemoveItem }) => {
|
|||
</Col>
|
||||
<Col className="col-12 col-md-4 text-right py-2 pr-md-3 pl-0">
|
||||
{currency}
|
||||
{totalCartPrice}
|
||||
{cartTotal}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
|
|
|||
|
|
@ -162,7 +162,9 @@ import {
|
|||
faVolumeUp,
|
||||
faSpinner,
|
||||
faPlayCircle,
|
||||
faPauseCircle
|
||||
faPauseCircle,
|
||||
faStopCircle,
|
||||
faInfoCircle
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
//import { faAcousticGuitar } from "../icons";
|
||||
|
|
@ -294,6 +296,7 @@ library.add(
|
|||
faSpinner,
|
||||
faPlayCircle,
|
||||
faPauseCircle,
|
||||
faStopCircle,
|
||||
|
||||
// Brand
|
||||
faFacebook,
|
||||
|
|
@ -310,6 +313,7 @@ library.add(
|
|||
faYoutube,
|
||||
faVideo,
|
||||
faInfo,
|
||||
faInfoCircle,
|
||||
faPhone,
|
||||
faTrello,
|
||||
|
||||
|
|
|
|||
|
|
@ -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,7 +410,16 @@ 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) => {
|
||||
|
|
@ -415,7 +430,7 @@ export const addJamtrackToShoppingCart = (options = {}) => {
|
|||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getShoppingCart = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -423,7 +438,7 @@ export const getShoppingCart = () => {
|
|||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const removeShoppingCart = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -434,7 +449,7 @@ export const removeShoppingCart = (options = {}) => {
|
|||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getBillingInfo = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -442,4 +457,58 @@ export const getBillingInfo = () => {
|
|||
.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));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
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 [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]);
|
||||
|
||||
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};
|
||||
};
|
||||
|
||||
export default useBrowserMedia;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// import { useCookies } from 'react-cookie';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import Cookies from 'universal-cookie';
|
||||
|
||||
export const useCheckout = () => {
|
||||
const cookieName = 'preserve_billing';
|
||||
// const [setCookie, removeCookie, cookies] = useCookies([cookieName]);
|
||||
const cookies = new Cookies(null, { path: '/' });
|
||||
|
||||
const setPreserveBillingInfo = () => {
|
||||
const date = new Date();
|
||||
const minutes = 10;
|
||||
date.setTime(date.getTime() + minutes * 60 * 1000);
|
||||
//expires if there is a cookie with the same name
|
||||
//removeCookie(cookieName, { path: '/' });
|
||||
cookies.remove(cookieName, { path: '/' });
|
||||
//set the new cookie
|
||||
//setCookie(cookieName, 'jam', { path: '/', expires: date });
|
||||
cookies.set(cookieName, 'jam', { path: '/', expires: date });
|
||||
};
|
||||
|
||||
const shouldPreserveBillingInfo = useMemo(() => {
|
||||
return cookies.get(cookieName) === 'jam';
|
||||
}, [cookies]);
|
||||
|
||||
const refreshPreserveBillingInfo = () => {
|
||||
if (shouldPreserveBillingInfo) {
|
||||
setPreserveBillingInfo();
|
||||
}
|
||||
}
|
||||
|
||||
const deletePreserveBillingInfo = () => {
|
||||
cookies.remove(cookieName, { path: '/' });
|
||||
if(cookies.get(cookieName)){
|
||||
console.log('after deleting the preserve billing cookie, it still exists!');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
setPreserveBillingInfo,
|
||||
shouldPreserveBillingInfo,
|
||||
refreshPreserveBillingInfo,
|
||||
deletePreserveBillingInfo
|
||||
}
|
||||
};
|
||||
|
|
@ -5,6 +5,7 @@ export const useShoppingCart = () => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [shoppingCart, setShoppingCart] = useState([]);
|
||||
const [error, setError] = useState(null);
|
||||
const TAX_RATE = 0.1;
|
||||
|
||||
useEffect(() => {
|
||||
getCartItems();
|
||||
|
|
@ -16,6 +17,8 @@ export const useShoppingCart = () => {
|
|||
return totalPrice;
|
||||
}, [shoppingCart]);
|
||||
|
||||
|
||||
|
||||
const getCartItems = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
|
@ -52,7 +55,7 @@ export const useShoppingCart = () => {
|
|||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return{
|
||||
shoppingCart, error, loading, removeCartItem, addCartItem, cartTotal
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import profileEN from './locales/en/profile.json'
|
|||
import accountEN from './locales/en/account.json'
|
||||
import affiliateEN from './locales/en/affiliate.json'
|
||||
import jamTracksEn from './locales/en/jamtracks.json'
|
||||
import checkoutEN from './locales/en/checkout.json'
|
||||
import checkoutSuccessEN from './locales/en/checkout_success.json'
|
||||
|
||||
import commonTranslationsES from './locales/es/common.json'
|
||||
import homeTranslationsES from './locales/es/home.json'
|
||||
|
|
@ -24,6 +26,8 @@ import profileES from './locales/es/profile.json'
|
|||
import accountES from './locales/es/account.json'
|
||||
import affiliateES from './locales/es/affiliate.json'
|
||||
import jamTracksEs from './locales/es/jamtracks.json'
|
||||
import checkoutES from './locales/es/checkout.json'
|
||||
import checkoutSuccessES from './locales/es/checkout_success.json'
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
fallbackLng: 'en',
|
||||
|
|
@ -40,7 +44,9 @@ i18n.use(initReactI18next).init({
|
|||
account: accountEN,
|
||||
friends: friendsTranslationsEN,
|
||||
affiliate: affiliateEN,
|
||||
jamtracks: jamTracksEn
|
||||
jamtracks: jamTracksEn,
|
||||
checkout: checkoutEN,
|
||||
checkoutSuccess: checkoutSuccessEN
|
||||
},
|
||||
es: {
|
||||
common: commonTranslationsES,
|
||||
|
|
@ -53,7 +59,9 @@ i18n.use(initReactI18next).init({
|
|||
account: accountES,
|
||||
friends: friendsTranslationsES,
|
||||
affiliate: affiliateES,
|
||||
jamtracks: jamTracksEs
|
||||
jamtracks: jamTracksEs,
|
||||
checkout: checkoutES,
|
||||
checkoutSuccess: checkoutSuccessES
|
||||
}
|
||||
},
|
||||
//ns: ['translations'],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"page_title": "Thank You!",
|
||||
"my_jamtracks": "My JamTracks"
|
||||
}
|
||||
|
|
@ -5,5 +5,40 @@
|
|||
"title": "Search",
|
||||
"placeholder": "Search by artist, song, style, or keyword"
|
||||
}
|
||||
},
|
||||
"my": {
|
||||
"page_title": "My JamTracks",
|
||||
"empty": {
|
||||
"title": "You haven't purchased any JamTracks yet",
|
||||
"description": "Browse our selection of JamTracks and find the perfect track to jam along with."
|
||||
},
|
||||
"search_input": {
|
||||
"title": "Search",
|
||||
"placeholder": "Enter song or artist name"
|
||||
}
|
||||
},
|
||||
"jamtrack": {
|
||||
"player": {
|
||||
"title": "JamTrack Player",
|
||||
"play": "Play",
|
||||
"pause": "Pause",
|
||||
"master_mix": "Master Mix"
|
||||
},
|
||||
"my_mixes": {
|
||||
"title": "My Mixes",
|
||||
"description": "Create New Mix",
|
||||
"Mixes": "Mixes",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"create_mix": {
|
||||
"title": "Create a Mix",
|
||||
"description": "Create a new mix by adjusting the volume of each instrument.",
|
||||
"tracks": "Tracks",
|
||||
"mute": "Mute",
|
||||
"tempo": "Tempo",
|
||||
"pitch": "Pitch",
|
||||
"mix_name": "Mix Name",
|
||||
"create": "Create Mix"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"page_title": "Thank You!"
|
||||
}
|
||||
|
|
@ -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'},
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ mixins.push(Reflux.listenTo(BrowserMediaPlaybackStore,"onMediaStateChanged"))
|
|||
tempos : [ 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208 ]
|
||||
|
||||
onMediaStateChanged: (changes) ->
|
||||
console.log("BrowserMediaControls onMediaStateChanged", changes)
|
||||
if changes.playbackStateChanged
|
||||
if @state.controls?
|
||||
if changes.playbackState == 'play_start'
|
||||
|
|
|
|||
|
|
@ -168,7 +168,6 @@ BrowserMediaActions = @BrowserMediaActions
|
|||
# let's check and see if we've asked the BrowserMediaStore to load this particular file or not
|
||||
|
||||
if @jamTrack?.activeStem
|
||||
|
||||
if @browserMediaState?.id != @jamTrack.activeStem.id
|
||||
new window.Fingerprint2().get((result, components) => (
|
||||
BrowserMediaActions.load(@jamTrack.activeStem.id, [window.location.protocol + '//' + window.location.host + "/api/jamtracks/#{@jamTrack.id}/stems/#{@jamTrack.activeStem.id}/download.mp3?file_type=mp3&mark=#{result}"], 'jamtrack_web_player')
|
||||
|
|
@ -184,7 +183,6 @@ BrowserMediaActions = @BrowserMediaActions
|
|||
|
||||
|
||||
else if @jamTrack?.activeMixdown
|
||||
|
||||
# if we don't have this on the server yet, don't engage the rest of this logic...
|
||||
return if @jamTrack.activeMixdown?.myPackage?.signing_state != 'SIGNED'
|
||||
|
||||
|
|
@ -205,7 +203,6 @@ BrowserMediaActions = @BrowserMediaActions
|
|||
@jamTrack.activeMixdown.client_state = 'downloading'
|
||||
|
||||
else if @jamTrack?
|
||||
|
||||
masterTrack = null
|
||||
for jamTrackTrack in @jamTrack.tracks
|
||||
if jamTrackTrack.track_type == 'Master'
|
||||
|
|
|
|||
Loading…
Reference in New Issue