WIP: jamtracks, shopping cart and checkout pages

This commit is contained in:
Nuwan 2024-07-13 15:17:13 +05:30
parent adafcb8569
commit a3c511d2b0
38 changed files with 1122 additions and 276 deletions

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { Card, CardBody } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import JKJamTracksAutoComplete from '../jamtracks/JKJamTracksAutoComplete';
import { getJamTracks, getAffiliatePartnerData } from '../../helpers/rest';
import { getJamTracks, getAffiliatePartnerData, autocompleteJamTracks } from '../../helpers/rest';
import { useAuth } from '../../context/UserAuth';
import { useHistory } from "react-router-dom";
import { useResponsive } from '@farfetch/react-context-responsive';
@ -118,6 +118,7 @@ const JKAffiliateLinks = () => {
<p>{t('links.jamtracks_pages_paragraph')}</p>
<div className='mt-4'>
<JKJamTracksAutoComplete
fetchFunc={autocompleteJamTracks}
onSelect={handleOnSelect}
onEnter={handleOnEnter}
showDropdown={showDropdown}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { Card, CardBody, Row, Col } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import JKJamTracksAutoComplete from './JKJamTracksAutoComplete';
import { getJamTracks, getJamTrackArtists } from '../../helpers/rest';
import { getJamTracks, getJamTrackArtists, autocompleteJamTracks } from '../../helpers/rest';
import JKJamTrackArtists from './JKJamTrackArtists';
import JKJamTracksList from './JKJamTracksList';
@ -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}

View File

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

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import JKModalDialog from '../common/JKModalDialog';
import JKProfileAvatar from './JKProfileAvatar';
import { useAuth } from '../../context/UserAuth';
import { getUserDetails } from '../../helpers/rest';
import { getUserDetail } from '../../helpers/rest';
const JKProfileAvatarUpload = ({show, toggle}) => {
const { t } = useTranslation('profile');
@ -14,7 +14,7 @@ const JKProfileAvatarUpload = ({show, toggle}) => {
useEffect(() => {
if(currentUser) {
console.log(currentUser.photo_url);
// getUserDetails(currentUser.id).then(response => {
// getUserDetail(currentUser.id).then(response => {
// console.log('_userDetails', response);
// });
}

View File

@ -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 &amp; Pay button to checkout.
</p>
</div>
<div className='d-flex'>
<Button onClick={() => setHideBillingInfo(!hideBillingInfo)}>
{hideBillingInfo ? 'Change Payment Info' : 'Hide Payment Info'}
</Button>
<Button onClick={ doPlaceOrder } className="ml-2">
Confirm &amp; Pay
</Button>
</div>
</div>
)}
<form onSubmit={handleSubmit(onSubmit)} className={hideBillingInfo ? 'd-none' : 'd-block'}>
{hasRedeemableJamTrack ? (
<div className="alert alert-info d-flex" role="alert">
<FontAwesomeIcon icon="info-circle" className="mr-2" />
<p>
Please enter your billing address and payment information below.&nbsp;{' '}
<strong>You will not be billed for any charges of any kind without your explicit authorization.</strong>
&nbsp; There are no "hidden" charges or fees, thank you!
</p>
</div>
) : (
<div className="alert alert-info d-flex" role="alert">
<FontAwesomeIcon icon="info-circle" className="mr-2" />
<p>Please enter your billing address and payment information below.&nbsp; </p>
</div>
)}
<Card className="mb-3">
<FalconCardHeader title="Billing Address" titleTag="h5" />
<CardBody>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-right">
<Label for="first_name" className={labelClassName.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 &amp; Pay
</Button>
<p className="fs--1 mt-3 mb-0">

View File

@ -0,0 +1,32 @@
import React from 'react'
import { Card, CardBody } from 'reactstrap'
import FalconCardHeader from '../common/FalconCardHeader'
import { useTranslation } from "react-i18next";
import { Link } from 'react-router-dom'
const JKCheckoutSuccess = () => {
const {t} = useTranslation('checkoutSuccess')
return (
<Card>
<FalconCardHeader title={t('page_title')} titleClass="font-weight-semi-bold" />
<CardBody className="pt-0 text-center mt-4">
<p className="text-muted">Thank you for your order! We'll send you an order confirmation email shortly.</p>
<p>
Click the button below to start using your new JamTracks.
</p>
<p>
<Link to="/jamtracks/my" className="btn btn-primary">
{t('my_jamtracks')}
</Link>
</p>
<div>
<p>
You can also play with your JamTracks in the <a href="https://www.jamkazam.com/downloads" target='_blank'>JamKazam desktop app</a>, available for Windows and Mac.
</p>
</div>
</CardBody>
</Card>
)
}
export default JKCheckoutSuccess

View File

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

View File

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

View File

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

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

View File

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

View File

@ -0,0 +1,46 @@
// import { useCookies } from 'react-cookie';
import { useMemo } from 'react';
import Cookies from 'universal-cookie';
export const useCheckout = () => {
const cookieName = 'preserve_billing';
// const [setCookie, removeCookie, cookies] = useCookies([cookieName]);
const cookies = new Cookies(null, { path: '/' });
const setPreserveBillingInfo = () => {
const date = new Date();
const minutes = 10;
date.setTime(date.getTime() + minutes * 60 * 1000);
//expires if there is a cookie with the same name
//removeCookie(cookieName, { path: '/' });
cookies.remove(cookieName, { path: '/' });
//set the new cookie
//setCookie(cookieName, 'jam', { path: '/', expires: date });
cookies.set(cookieName, 'jam', { path: '/', expires: date });
};
const shouldPreserveBillingInfo = useMemo(() => {
return cookies.get(cookieName) === 'jam';
}, [cookies]);
const refreshPreserveBillingInfo = () => {
if (shouldPreserveBillingInfo) {
setPreserveBillingInfo();
}
}
const deletePreserveBillingInfo = () => {
cookies.remove(cookieName, { path: '/' });
if(cookies.get(cookieName)){
console.log('after deleting the preserve billing cookie, it still exists!');
}
}
return {
setPreserveBillingInfo,
shouldPreserveBillingInfo,
refreshPreserveBillingInfo,
deletePreserveBillingInfo
}
};

View File

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

View File

@ -12,6 +12,8 @@ import profileEN from './locales/en/profile.json'
import accountEN from './locales/en/account.json'
import affiliateEN from './locales/en/affiliate.json'
import jamTracksEn from './locales/en/jamtracks.json'
import checkoutEN from './locales/en/checkout.json'
import checkoutSuccessEN from './locales/en/checkout_success.json'
import commonTranslationsES from './locales/es/common.json'
import homeTranslationsES from './locales/es/home.json'
@ -24,6 +26,8 @@ import profileES from './locales/es/profile.json'
import accountES from './locales/es/account.json'
import affiliateES from './locales/es/affiliate.json'
import jamTracksEs from './locales/es/jamtracks.json'
import checkoutES from './locales/es/checkout.json'
import checkoutSuccessES from './locales/es/checkout_success.json'
i18n.use(initReactI18next).init({
fallbackLng: 'en',
@ -40,7 +44,9 @@ i18n.use(initReactI18next).init({
account: accountEN,
friends: friendsTranslationsEN,
affiliate: affiliateEN,
jamtracks: jamTracksEn
jamtracks: jamTracksEn,
checkout: checkoutEN,
checkoutSuccess: checkoutSuccessEN
},
es: {
common: commonTranslationsES,
@ -53,7 +59,9 @@ i18n.use(initReactI18next).init({
account: accountES,
friends: friendsTranslationsES,
affiliate: affiliateES,
jamtracks: jamTracksEs
jamtracks: jamTracksEs,
checkout: checkoutES,
checkoutSuccess: checkoutSuccessES
}
},
//ns: ['translations'],

View File

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

View File

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

View File

@ -5,5 +5,40 @@
"title": "Search",
"placeholder": "Search by artist, song, style, or keyword"
}
},
"my": {
"page_title": "My JamTracks",
"empty": {
"title": "You haven't purchased any JamTracks yet",
"description": "Browse our selection of JamTracks and find the perfect track to jam along with."
},
"search_input": {
"title": "Search",
"placeholder": "Enter song or artist name"
}
},
"jamtrack": {
"player": {
"title": "JamTrack Player",
"play": "Play",
"pause": "Pause",
"master_mix": "Master Mix"
},
"my_mixes": {
"title": "My Mixes",
"description": "Create New Mix",
"Mixes": "Mixes",
"actions": "Actions"
},
"create_mix": {
"title": "Create a Mix",
"description": "Create a new mix by adjusting the volume of each instrument.",
"tracks": "Tracks",
"mute": "Mute",
"tempo": "Tempo",
"pitch": "Pitch",
"mix_name": "Mix Name",
"create": "Create Mix"
}
}
}

View File

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

View File

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

View File

@ -37,7 +37,7 @@ export const jamTrackRoutes = {
exact: true,
icon: 'record-vinyl',
children: [
{ to: '/jamtracks/my', name: 'My JamTracks'},
{ to: '/my-jamtracks', name: 'My JamTracks'},
{ to: '/jamtracks', name: 'Find JamTracks'},
]
}

View File

@ -36,6 +36,7 @@ mixins.push(Reflux.listenTo(BrowserMediaPlaybackStore,"onMediaStateChanged"))
tempos : [ 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208 ]
onMediaStateChanged: (changes) ->
console.log("BrowserMediaControls onMediaStateChanged", changes)
if changes.playbackStateChanged
if @state.controls?
if changes.playbackState == 'play_start'

View File

@ -168,7 +168,6 @@ BrowserMediaActions = @BrowserMediaActions
# let's check and see if we've asked the BrowserMediaStore to load this particular file or not
if @jamTrack?.activeStem
if @browserMediaState?.id != @jamTrack.activeStem.id
new window.Fingerprint2().get((result, components) => (
BrowserMediaActions.load(@jamTrack.activeStem.id, [window.location.protocol + '//' + window.location.host + "/api/jamtracks/#{@jamTrack.id}/stems/#{@jamTrack.activeStem.id}/download.mp3?file_type=mp3&mark=#{result}"], 'jamtrack_web_player')
@ -184,7 +183,6 @@ BrowserMediaActions = @BrowserMediaActions
else if @jamTrack?.activeMixdown
# if we don't have this on the server yet, don't engage the rest of this logic...
return if @jamTrack.activeMixdown?.myPackage?.signing_state != 'SIGNED'
@ -205,7 +203,6 @@ BrowserMediaActions = @BrowserMediaActions
@jamTrack.activeMixdown.client_state = 'downloading'
else if @jamTrack?
masterTrack = null
for jamTrackTrack in @jamTrack.tracks
if jamTrackTrack.track_type == 'Master'