Merge branch 'develop' of bitbucket.org:jamkazam/jam-cloud into develop

This commit is contained in:
Seth Call 2024-11-30 13:17:28 -06:00
commit 9b6f6f74e9
65 changed files with 1015 additions and 320 deletions

View File

@ -21,7 +21,7 @@ describe('Affiliate Earnings', () => {
}).as('fetchAllEarnings');
cy.visit('/affiliate/earnings');
cy.wait('@fetchAllEarnings');
cy.contains('No Records!');
cy.contains('There is no earnings data yet ');
});
});
@ -68,7 +68,7 @@ describe('Affiliate Earnings', () => {
cy.wait('@fetchAllEarnings');
cy.get('[data-testid=affiliateEarningsList] tbody tr:first-child td')
.eq(0)
.should('have.text', 'September - 2021');
.should('have.text', 'September 2021');
cy.get('[data-testid=affiliateEarningsList] tbody tr:first-child td')
.eq(1)
.should('have.text', 'Silver - 2');

View File

@ -22,7 +22,7 @@ describe('affiliate signups', () => {
}).as('fetchAllSignups');
cy.visit('/affiliate/signups');
cy.wait('@fetchAllSignups');
cy.contains('No Records!');
cy.contains('There is no signup data yet ');
});
});

View File

@ -0,0 +1,54 @@
///<reference types="cypress" />
import makeFakeUser from '../../factories/user';
describe('forgot password', () => {
beforeEach(() => {
const currentUser = makeFakeUser({
email: 'sam@example.com'
});
cy.clearCookie('remeber_token');
});
it('redirects to forgot password page', () => {
cy.visit('/');
cy.url().should('include', '/authentication/basic/login');
cy.get('a')
.contains('Forgot password?')
.click();
cy.url().should('include', '/authentication/basic/forget-password');
cy.get('h5').contains('Forgot Your Password');
});
describe('validate forgot password form', () => {
beforeEach(() => {
cy.visit('/authentication/basic/forget-password');
cy.get('[data-testid=email]').clear();
cy.get('[data-testid=submit]').should('be.disabled');
});
//invalid email format
it('invalid email format', () => {
cy.get('[data-testid=email]').type('invalid-email-format@example');
cy.get('[data-testid=submit]').click();
cy.url().should('not.include', /\/authentication\/basic\/confirm-mail?\S+/);
});
//valid email format but non-existing
it('valid email format but non-existing', () => {
cy.get('[data-testid=email]').type('valid-email-format@example.com');
cy.get('[data-testid=submit]').click();
cy.url().should('not.include', /\/authentication\/basic\/confirm-mail?\S+/);
});
//valid and existing email
it('valid and existing email', () => {
cy.get('[data-testid=email]').type('nuwan@jamkazam.com');
cy.get('[data-testid=submit]').click();
cy.wait(3000);
cy.contains('Please check your email!');
cy.url().should('match', /\S+authentication\/basic\/confirm-mail?\S+/);
cy.contains('An email has been sent to nuwan@jamkazam.com.')
});
});
});

View File

@ -30,9 +30,9 @@ function submitLogin(){
describe('Unauthenticated users redirect to login page', () => {
it('redirects to login page', () => {
cy.clearCookie('remeber_token')
cy.visit('/friends')
cy.visit('/')
cy.url().should('include', '/authentication/basic/login')
cy.contains('Sign in')
cy.contains('Sign In')
})
})
@ -81,4 +81,13 @@ describe('Login page', () => {
})
describe('Forget password page', () => {
it('submit forget password form', () => {
cy.visit('/authentication/basic/forget-password')
cy.get('[data-testid=email]').type('peter@example.com')
cy.get('[data-testid=submit]').click()
cy.contains('An email is sent')
})
})

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

@ -10,3 +10,4 @@
@import './custom/form';
@import './custom/chips';
@import './custom/common';
@import './custom/partner_agreement_v1';

View File

@ -0,0 +1,298 @@
#partner-agreement-v1 {
* {
color:black! important;
}
.c1, .c20 {
border-bottom-color: #CCCCCC ! important;
border-top-color: #CCCCCC ! important;
border-left-color: #CCCCCC ! important;
border-right-color: #CCCCCC ! important;
}
.c12 {
color: #CCCCCC ! important;
}
ol {
margin: 0;
padding: 0
}
li {
color: #000000;
font-size: 10pt;
font-family: "Times New Roman";
margin-bottom: 1em;
}
.c1 {
border-bottom-width: 1pt;
border-top-style: solid;
width: 193.2pt;
border-right-style: solid;
padding: 0pt 5.8pt 0pt 5.8pt;
border-bottom-color: #000000;
border-top-width: 1pt;
border-bottom-style: solid;
vertical-align: top;
border-top-color: #000000;
border-left-color: #000000;
border-right-color: #000000;
border-left-style: solid;
border-right-width: 1pt;
border-left-width: 1pt
}
.c20 {
border-bottom-width: 1pt;
border-top-style: solid;
width: 238.2pt;
border-right-style: solid;
padding: 0pt 5.8pt 0pt 5.8pt;
border-bottom-color: #000000;
border-top-width: 1pt;
border-bottom-style: solid;
vertical-align: top;
border-top-color: #000000;
border-left-color: #000000;
border-right-color: #000000;
border-left-style: solid;
border-right-width: 1pt;
border-left-width: 1pt
}
.c3 {
padding-left: 0pt;
line-height: 1.0;
padding-top: 0pt;
margin-left: 36pt;
padding-bottom: 0pt
}
.c12 {
vertical-align: baseline;
color: #000000;
font-style: normal;
text-decoration: none
}
.c4 {
line-height: 1.0;
padding-top: 0pt;
text-align: left;
padding-bottom: 0pt
}
.c11 {
max-width: 432pt;
background-color: #ffffff;
padding: 72pt 90pt 72pt 90pt
}
.c2 {
widows: 2;
orphans: 2;
direction: ltr
}
.c16 {
margin-right: auto;
border-collapse: collapse;
margin-left: -5.8pt
}
.c9 {
line-height: 1.0;
padding-top: 36pt;
padding-bottom: 0pt
}
.c21 {
line-height: 1.0;
padding-top: 0pt;
padding-bottom: 36pt
}
.c15 {
font-size: 14pt;
font-family: "Calibri"
}
.c18 {
font-size: 12pt;
font-family: "Times New Roman"
}
.c6 {
margin: 0;
padding: 0
}
.c0 {
font-size: 12pt;
font-family: "Calibri"
}
.c22 {
font-size: 10pt;
font-family: "Times New Roman"
}
.c13 {
color: #000000;
background-color: #ffff00
}
.c14 {
font-size: 9pt;
font-family: "Times New Roman"
}
.c10 {
text-decoration: underline
}
.c17 {
color: #ff0000
}
.c7 {
font-weight: bold
}
.c19 {
height: 0pt
}
.c8 {
font-weight: normal
}
.c5 {
height: 10pt
}
.title {
widows: 2;
padding-top: 0pt;
line-height: 1.0;
orphans: 2;
text-align: center;
color: #000000;
font-size: 14pt;
font-family: "Times New Roman";
font-weight: bold;
padding-bottom: 0pt;
page-break-after: avoid
}
.subtitle {
widows: 2;
padding-top: 18pt;
line-height: 1.0;
orphans: 2;
text-align: left;
color: #666666;
font-style: italic;
font-size: 24pt;
font-family: "Georgia";
padding-bottom: 4pt;
page-break-after: avoid
}
p {
color: #000000;
font-size: 10pt;
margin: 0;
font-family: "Times New Roman"
}
h1 {
widows: 2;
padding-top: 0pt;
line-height: 1.0;
orphans: 2;
text-align: center;
color: #000000;
font-size: 12pt;
font-family: "Times New Roman";
font-weight: bold;
padding-bottom: 0pt;
page-break-after: avoid
}
h2 {
widows: 2;
padding-top: 0pt;
line-height: 1.0;
orphans: 2;
text-align: left;
color: #000000;
font-size: 10pt;
font-family: "Times New Roman";
font-weight: bold;
padding-bottom: 0pt;
page-break-after: avoid
}
h3 {
widows: 2;
padding-top: 0pt;
line-height: 1.0;
orphans: 2;
text-align: left;
color: #000000;
font-size: 10pt;
text-decoration: underline;
font-family: "Times New Roman";
padding-bottom: 0pt;
page-break-after: avoid
}
h4 {
widows: 2;
padding-top: 12pt;
line-height: 1.0;
orphans: 2;
text-align: left;
color: #000000;
font-size: 12pt;
font-family: "Times New Roman";
font-weight: bold;
padding-bottom: 2pt;
page-break-after: avoid
}
h5 {
widows: 2;
padding-top: 11pt;
line-height: 1.0;
orphans: 2;
text-align: left;
color: #000000;
font-size: 11pt;
font-family: "Times New Roman";
font-weight: bold;
padding-bottom: 2pt;
page-break-after: avoid
}
h6 {
widows: 2;
padding-top: 10pt;
line-height: 1.0;
orphans: 2;
text-align: left;
color: #000000;
font-size: 10pt;
font-family: "Times New Roman";
font-weight: bold;
padding-bottom: 2pt;
page-break-after: avoid
}
}

View File

@ -152,7 +152,7 @@
.jamtrack-track {
position: relative;
background: #ddd;
// background: #ddd;
}
.jamtrack-track.jamtrack-track-0 {

View File

@ -5,7 +5,7 @@ const AgreementText = () => {
<>
<div id="partner-agreement-v1">
<p className="c2">
<span className="c0">Updated: February 9, 2021.</span>
<span className="c0">Updated: November 18, 2024.</span>
</p>
<p className="c2 c5">
@ -37,7 +37,7 @@ const AgreementText = () => {
<span className="c0">&rdquo; means a website. &ldquo;</span>
<span className="c0 c10">JamKazam Site</span>
<span className="c0">
&rdquo; means the jamkazam.com website or a JamKazam applicaion or any other site that is owned or operated
&rdquo; means the jamkazam.com website or a JamKazam application or any other site that is owned or operated
by or on behalf of us and which is identified as participating in the Program in the{' '}
</span>
<span className="c0 c10 c17">Program Advertising Fee Schedule</span>
@ -68,7 +68,7 @@ const AgreementText = () => {
&ldquo;
</span>
<span className="c0 c10">Product</span>
<span className="c0">&rdquo; a product or service sold on the JamKazam Site and listed in the </span>
<span className="c0">&rdquo; is a product or service sold on the JamKazam Site and listed in the </span>
<span className="c0 c10 c17">Program Advertising Fee Schedule</span>
<span className="c0">
&nbsp;in Section 10. In order to facilitate your advertisement of Products, we may make available to you
@ -533,8 +533,7 @@ const AgreementText = () => {
<li className="c3 c2">
<div className="c0 c8">Product: JamTracks</div>
<div className="c2">
JamKazam will pay US$0.25 per JamTrack sold as a Qualifying Purchase by Qualifying Customers of these
Products.
JamKazam will pay advertising fees of 30% of the net revenues from Qualifying Purchases by Qualifying Customers of these Products.
</div>
</li>
</ul>
@ -979,7 +978,7 @@ const AgreementText = () => {
<div>
<p className="c2 c21">
<span className="c8 c14">JamKazam Confidential&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span className="c14 c8">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;02/09/2021</span>
<span className="c14 c8">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;11/18/2024</span>
</p>
</div>
</div>

View File

@ -11,17 +11,17 @@ const JKAffiliateEarning = ({ payment }) => {
{greaterThan.sm ? (
<tr>
<td>
{monthName(payment.month - 1)} - {payment.year}
{monthName(payment.month - 1)} {payment.year}
</td>
<td>
<td className='text-center'>
{payment.subscriptions.map((subscription, index) => (
<div key={index}>
{getDisplayName(subscription.plan)} - {subscription.count}
</div>
))}
</td>
<td>{payment.jamtracks_sold}</td>
<td>${(payment.due_amount_in_cents / 100).toFixed(2)}</td>
<td className='text-center'>{payment.jamtracks_sold}</td>
<td className='text-center'>${(payment.due_amount_in_cents / 100).toFixed(2)}</td>
</tr>
) : (
<>

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Card, CardBody, Row, Col, Alert } from 'reactstrap';
import { Card, CardBody, Row, Col } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import { getAffiliatePayments } from '../../helpers/rest';
@ -8,8 +8,8 @@ import { isIterableArray } from '../../helpers/utils';
import { useResponsive } from '@farfetch/react-context-responsive';
import JKAffiliateEarningsList from './JKAffiliateEarningsList';
import JKAffiliateEarningsSwiper from './JKAffiliateEarningsSwiper';
import Loader from '../common/Loader';
import { Link } from 'react-router-dom';
const JKAffiliateEarnings = () => {
const { t } = useTranslation('affiliate');
@ -57,9 +57,11 @@ const JKAffiliateEarnings = () => {
) : (
<Row className="p-card">
<Col>
<Alert color="info" className="mb-0">
{t('no_records', { ns: 'common' })}
</Alert>
{/* <Alert color="info" className="mb-0"> */}
{t('earnings.no_earnings_data')}
<Link to="/affiliate/links">{t('earnings.share_affiliate_links')}</Link>
{t('earnings.start_to_earn')}
{/* </Alert> */}
</Col>
</Row>
)}

View File

@ -10,9 +10,9 @@ const JKAffiliateEarningsList = ({ payments }) => {
<thead>
<tr>
<th>{t('earnings.list.header.date')}</th>
<th>{t('earnings.list.header.subscriptions')}</th>
<th>{t('earnings.list.header.jamtracks')}</th>
<th>{t('earnings.list.header.earnings')}</th>
<th className='text-center'>{t('earnings.list.header.subscriptions')}</th>
<th className='text-center'>{t('earnings.list.header.jamtracks')}</th>
<th className='text-center'>{t('earnings.list.header.earnings')}</th>
</tr>
</thead>
<tbody>

View File

@ -1,11 +1,11 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { Card, CardBody } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import JKJamTracksAutoComplete from '../jamtracks/JKJamTracksAutoComplete';
import { getJamTracks, getAffiliatePartnerData, autocompleteJamTracks } from '../../helpers/rest';
import { useAuth } from '../../context/UserAuth';
import { useHistory } from "react-router-dom";
import { useHistory } from 'react-router-dom';
import { useResponsive } from '@farfetch/react-context-responsive';
const JKAffiliateLinks = () => {
@ -25,20 +25,19 @@ const JKAffiliateLinks = () => {
}, []);
const fetchAffiliate = async () => {
try{
try {
const response = await getAffiliatePartnerData(currentUser.id);
const affiliate = await response.json();
if (affiliate) {
setAffiliate(affiliate);
setAffiliatePartnerId(affiliate.account.partner_id);
setAffiliatePartnerId(affiliate.account.id);
}
}catch(error){
} catch (error) {
console.log(error);
}
}
};
const fetchJamTracks = async options => {
try {
const resp = await getJamTracks(options);
const data = await resp.json();
@ -70,26 +69,14 @@ const JKAffiliateLinks = () => {
return options;
};
const homePageLink = `https://www.jamkazam.com?affiliate=${affiliatePartnerId}`;
const handleClickOnHomePageLink = () => {
if(affiliate){
window.open(homePageLink, '_blank');
}else{
history.push('/affiliate/agreement');
}
}
const homePageLink = useMemo(() => `${process.env.REACT_APP_CLIENT_BASE_URL}?affiliate=${affiliatePartnerId}`, [affiliatePartnerId]);
const jamTrackLink = jamTrack => {
return `https://jamkazam.com/jamtrack/landing/${jamTrack.plan_code}?affiliate=${affiliatePartnerId}`;
}
const handleOnClickJamTrackLink = () => {
if(affiliate){
window.open(jamTrackLink, '_blank');
}else{
history.push('/affiliate/agreement');
const jamTrackLink = useMemo(() => jamTrack => {
if (!jamTrack) {
return '';
}
}
return `${process.env.REACT_APP_CLIENT_BASE_URL}/jamtrack/landing/${jamTrack.plan_code}?affiliate=${affiliatePartnerId}`;
}, [affiliatePartnerId]);
//autocomplete related code
const handleOnSelect = selected => {
@ -102,32 +89,40 @@ const JKAffiliateLinks = () => {
console.log('onEnter', queryStr);
const params = queryOptions(queryStr);
fetchJamTracks(params);
}
};
return (
<Card style={{ width: greaterThan.sm ? '75%' : '100%'}} className="mx-auto affiliate-links">
<Card style={{ width: greaterThan.sm ? '75%' : '100%' }} className="mx-auto affiliate-links">
<FalconCardHeader title={t('links.page_title')} titleClass="font-weight-semi-bold" />
<CardBody className="pt-3 pb-5">
<p>{t('links.paragraph1')}</p>
<div className="affiliate-links__subtitle">{t('links.home_page_subtitle')}</div>
<p>
{t('links.home_page_paragraph')}:{' '}
<a href="#" onClick={handleClickOnHomePageLink}>{homePageLink}</a>
{affiliate ? (
<a href={homePageLink} target="_blank">
{homePageLink}
</a>
) : (
<a href="javascript:void(0);" onClick={() => history.push('/affiliate/agreement')}>
{homePageLink}
</a>
)}
</p>
<div className="affiliate-links__subtitle">{t('links.jamtracks_pages_subtitle')}</div>
<p>{t('links.jamtracks_pages_paragraph')}</p>
<div className='mt-4'>
<div className="mt-4">
<JKJamTracksAutoComplete
fetchFunc={autocompleteJamTracks}
onSelect={handleOnSelect}
onEnter={handleOnEnter}
showDropdown={showDropdown}
setShowDropdown={setShowDropdown}
inputValue={autoCompleteInputValue}
setInputValue={setAutoCompleteInputValue}
/>
fetchFunc={autocompleteJamTracks}
onSelect={handleOnSelect}
onEnter={handleOnEnter}
showDropdown={showDropdown}
setShowDropdown={setShowDropdown}
inputValue={autoCompleteInputValue}
setInputValue={setAutoCompleteInputValue}
/>
</div>
{jamTracks &&
{jamTracks &&
jamTracks.map(jamTrack => {
return (
<div key={jamTrack.id}>
@ -135,9 +130,15 @@ const JKAffiliateLinks = () => {
{t('links.affiliate_link_for')} {jamTrack.name} {t('links.by')} {jamTrack.original_artist}:
</div>
<div>
<a href="#" onClick={handleOnClickJamTrackLink}>
{jamTrackLink(jamTrack)}
</a>
{affiliate ? (
<a href={jamTrackLink(jamTrack)} target="_blank">
{jamTrackLink(jamTrack)}
</a>
) : (
<a href="javascript:void(0);" onClick={() => history.push('/affiliate/agreement')}>
{jamTrackLink(jamTrack)}
</a>
)}
</div>
</div>
);

View File

@ -3,6 +3,7 @@ import { Card, CardBody } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import { useResponsive } from '@farfetch/react-context-responsive';
import { Link } from 'react-router-dom';
const JKAffiliateProgram = () => {
const { t } = useTranslation('affiliate');
@ -17,9 +18,9 @@ const JKAffiliateProgram = () => {
<p>{t('program.paragraph3')}</p>
<p>
{t('program.paragraph4-1')}
<a href=""><strong>{t('program.click-here')}</strong></a>
<Link to="/affiliate/agreement"><strong>{t('program.click-here')}</strong></Link>
{t('program.paragraph4-2')}
<a href=""><strong>{t('program.click-here')}</strong></a>
<Link to="/affiliate/links"><strong>{t('program.click-here')}</strong></Link>
{t('program.paragraph4-3')}
</p>
</div>

View File

@ -9,8 +9,8 @@ const JKAffiliateSignup = ({signup}) => {
{greaterThan.sm ? (
<tr>
<td>{signup.month}</td>
<td>{signup.visits}</td>
<td>{signup.signups}</td>
<td className="text-center">{signup.visits}</td>
<td className="text-center">{signup.signups}</td>
</tr>
) : (
<>

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Card, CardBody, Row, Col, Alert } from 'reactstrap';
import { Card, CardBody, Row, Col } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import { getAffiliateSignups } from '../../helpers/rest';
@ -9,6 +9,7 @@ import Loader from '../common/Loader';
import JKAffiliateSignupsList from './JKAffiliateSignupsList';
import JKAffiliateSignupsSwiper from './JKAffiliateSignupsSwiper';
import { useResponsive } from '@farfetch/react-context-responsive';
import { Link } from 'react-router-dom/cjs/react-router-dom.min';
const JKAffiliateSignups = () => {
const { t } = useTranslation('affiliate');
@ -126,9 +127,11 @@ const JKAffiliateSignups = () => {
) : (
<Row className="p-card">
<Col>
<Alert color="info" className="mb-0">
{t('no_records', { ns: 'common' })}
</Alert>
{/* <Alert color="info" className="mb-0"> */}
{t('signups.no_signup_data')}
<Link to="/affiliate/links">{t('signups.share_affiliate_links')}</Link>
{t('signups.with_your_audience')}
{/* </Alert> */}
</Col>
</Row>
)}

View File

@ -12,7 +12,7 @@ const JKAffiliateSignupsList = ({ signups, nextPage, loading, stepForward }) =>
<th width="35%" scope="col">
{t('signups.list.header.date')}
</th>
<th width="35%" scope="col">
<th width="35%" scope="col" className="text-center">
{t('signups.list.header.visits')}
</th>
<th scope="col" className="text-center">

View File

@ -4,23 +4,32 @@ import { Button } from 'reactstrap';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import envelope from '../../assets/img/illustrations/envelope.png';
import { useBrowserQuery } from '../../context/BrowserQuery';
import { useTranslation } from 'react-i18next';
const ConfirmMailContent = ({ email, layout, titleTag: TitleTag }) => (
<Fragment>
<img className="d-block mx-auto mb-4" src={envelope} alt="sent" width={70} />
<TitleTag>Please check your email!</TitleTag>
<p>
An email has been sent to <strong>{email}</strong>. Please click on the included link to reset your password.
</p>
<Button tag={Link} color="primary" size="sm" className="mt-3" to={`/authentication/${layout}/login`}>
<FontAwesomeIcon icon="chevron-left" transform="shrink-4 down-1" className="mr-1" />
Return to login
</Button>
</Fragment>
);
const ConfirmMailContent = ({ layout, titleTag: TitleTag }) => {
const queryString = useBrowserQuery();
const { t } = useTranslation('auth');
const toWording = queryString && queryString.get('email') ? queryString.get('email') : t('confirmEmailContent.toYou');
return (
<Fragment>
<img className="d-block mx-auto mb-4" src={envelope} alt="sent" width={70} />
<TitleTag>{t('confirmEmailContent.title')}</TitleTag>
<p>{t('confirmEmailContent.description_1')} {toWording}. {t('confirmEmailContent.description_2')}</p>
<p>
{t('confirmEmailContent.description_3')}
</p>
<Button tag={Link} color="primary" size="sm" className="mt-3" to={`/authentication/${layout}/login`}>
<FontAwesomeIcon icon="chevron-left" transform="shrink-4 down-1" className="mr-1" />
{t('confirmEmailContent.returnToLogin')}
</Button>
</Fragment>
);
};
ConfirmMailContent.propTypes = {
email: PropTypes.string.isRequired,
layout: PropTypes.string,
titleTag: PropTypes.string
};

View File

@ -4,44 +4,60 @@ import { Link } from 'react-router-dom';
import { toast } from 'react-toastify';
import { Button, Form, FormGroup, Input } from 'reactstrap';
import withRedirect from '../../hoc/withRedirect';
import { requstResetForgotPassword } from '../../helpers/rest';
import { useTranslation } from 'react-i18next';
const ForgetPasswordForm = ({ setRedirect, setRedirectUrl, layout }) => {
// State
const [email, setEmail] = useState('');
const [submitting, setSubmitting] = useState(false);
const { t } = useTranslation('auth');
// Handler
const handleSubmit = e => {
e.preventDefault();
setSubmitting(true);
if (email) {
toast.success(`An email is sent to ${email} with password reset link`);
setRedirect(true);
requstResetForgotPassword(email)
.then(() => {
toast.success(`An email is sent to ${email} with password reset link`);
setRedirect(true);
})
.catch(error => {})
.finally(() => {
setSubmitting(false);
});
}
};
useEffect(() => {
setRedirectUrl(`/authentication/${layout}/confirm-mail`);
}, [setRedirectUrl, layout]);
setRedirectUrl(`/authentication/${layout}/confirm-mail?email=${email}`);
}, [setRedirectUrl, layout, email]);
return (
<Form className="mt-4" onSubmit={handleSubmit}>
<FormGroup>
<Input
className="form-control"
placeholder="Email address"
placeholder={t('forgotForm.email')}
value={email}
onChange={({ target }) => setEmail(target.value)}
type="email"
required
disabled={submitting}
data-testid="email"
/>
</FormGroup>
<FormGroup>
<Button color="primary" block disabled={!email}>
Send reset link
<Button color="primary" block disabled={!email || submitting} data-testid="submit">
{submitting ? t('forgotForm.submitting') : t('forgotForm.submit')}
</Button>
</FormGroup>
<Link className="fs--1 text-600" to="#!">
{/* <Link className="fs--1 text-600" to="#!">
I can't recover my account using this page
<span className="d-inline-block ml-1">&rarr;</span>
</Link>
</Link> */}
</Form>
);
};

View File

@ -8,6 +8,7 @@ import SocialAuthButtons from './SocialAuthButtons';
import withRedirect from '../../hoc/withRedirect';
import { useAuth } from '../../context/UserAuth';
//import { signin } from '../../services/authService';
import { useTranslation } from 'react-i18next';
const LoginForm = ({ setRedirect, hasLabel, layout }) => {
// State
@ -17,6 +18,8 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => {
const [isDisabled, setIsDisabled] = useState(true);
const history = useHistory();
const { t } = useTranslation('auth');
const location = useLocation();
let { from } = location.state || { from: { pathname: "/" } };
@ -27,8 +30,8 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => {
const handleSubmit = async e => {
e.preventDefault();
const credentials = {email, password}
setIsDisabled(true);
const user = await login(credentials)
console.log("handleSubmit", user);
if(user){
setCurrentUser(user)
//localStorage.setItem('user', user)
@ -40,7 +43,7 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => {
}else{
toast.error("Incorrect email or password");
}
setIsDisabled(false);
};
useEffect(() => {
@ -50,20 +53,20 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => {
return (
<Form onSubmit={handleSubmit}>
<FormGroup>
{hasLabel && <Label>Email address</Label>}
{hasLabel && <Label>{ t('signinForm.email')}</Label>}
<Input
data-testid="email"
placeholder={!hasLabel ? 'Email address' : ''}
placeholder={!hasLabel ? t('signinForm.email') : ''}
value={email}
onChange={({ target }) => setEmail(target.value)}
type="email"
/>
</FormGroup>
<FormGroup>
{hasLabel && <Label>Password</Label>}
{hasLabel && <Label>{t('signinForm.password')}</Label>}
<Input
data-testid="password"
placeholder={!hasLabel ? 'Password' : ''}
placeholder={!hasLabel ? t('signinForm.password') : ''}
value={password}
onChange={({ target }) => setPassword(target.value)}
type="password"
@ -73,22 +76,22 @@ const LoginForm = ({ setRedirect, hasLabel, layout }) => {
<Col xs="auto">
<CustomInput
id="customCheckRemember"
label="Remember me"
label={ t('signinForm.remember') }
checked={remember}
onChange={({ target }) => setRemember(target.checked)}
type="checkbox"
/>
</Col>
<Col xs="auto">
{/* <Link className="fs--1" to={`/authentication/${layout}/forget-password`}>
<Link className="fs--1" to={`/authentication/${layout}/forget-password`}>
Forget Password?
</Link> */}
<a href="https://www.jamkazam.com/request_reset_password" target='_blank'>Forgot Password</a>
</Link>
{/* <a href="https://www.jamkazam.com/request_reset_password" target='_blank'>Forgot Password</a> */}
</Col>
</Row>
<FormGroup>
<Button color="primary" block className="mt-3" data-testid="submit" disabled={isDisabled}>
Sign in
{ t('signinForm.submit') }
</Button>
</FormGroup>
{/* <Divider className="mt-4">or log in with</Divider>

View File

@ -5,18 +5,42 @@ import { Button, Form, FormGroup, Input } from 'reactstrap';
import withRedirect from '../../hoc/withRedirect';
import Label from 'reactstrap/es/Label';
import classNames from 'classnames';
import { useBrowserQuery } from '../../context/BrowserQuery';
import { resetForgotPassword } from '../../helpers/rest';
import { useTranslation } from 'react-i18next';
const PasswordResetForm = ({ setRedirect, setRedirectUrl, layout, hasLabel }) => {
// State
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isDisabled, setIsDisabled] = useState(true);
const queryString = useBrowserQuery();
const { t } = useTranslation('auth');
// Handler
const handleSubmit = e => {
e.preventDefault();
toast.success('Login with your new password');
setRedirect(true);
const token = queryString.get('token');
const email = queryString.get('email');
if (!token || !email) return;
if (password !== confirmPassword) return;
const data = { email, token, password, password_confirmation: confirmPassword}
setIsDisabled(true);
resetForgotPassword(data)
.then(() => {
toast.success(t('resetForm.successMessage'));
setRedirect(true);
})
.catch(error => {
error.json().then(data => {
toast.error(data.message);
});
})
.finally(() => {
setIsDisabled(false);
});
};
useEffect(() => {
@ -32,25 +56,25 @@ const PasswordResetForm = ({ setRedirect, setRedirectUrl, layout, hasLabel }) =>
return (
<Form className={classNames('mt-3', { 'text-left': hasLabel })} onSubmit={handleSubmit}>
<FormGroup>
{hasLabel && <Label>New Password</Label>}
{hasLabel && <Label>{t('resetForm.password')}</Label>}
<Input
placeholder={!hasLabel ? 'New Password' : ''}
placeholder={!hasLabel ? t('resetForm.password') : ''}
value={password}
onChange={({ target }) => setPassword(target.value)}
type="password"
/>
</FormGroup>
<FormGroup>
{hasLabel && <Label>Confirm Password</Label>}
{hasLabel && <Label>{t('resetForm.confirmPassword')}</Label>}
<Input
placeholder={!hasLabel ? 'Confirm Password' : ''}
placeholder={!hasLabel ? t('resetForm.confirmPassword') : ''}
value={confirmPassword}
onChange={({ target }) => setConfirmPassword(target.value)}
type="password"
/>
</FormGroup>
<Button color="primary" block className="mt-3" disabled={isDisabled}>
Set password
{t('resetForm.submit')}
</Button>
</Form>
);

View File

@ -3,7 +3,7 @@ import ConfirmMailContent from '../ConfirmMailContent';
const ConfirmMail = () => (
<div className="text-center">
<ConfirmMailContent email="xyz@abc.com" />
<ConfirmMailContent />
</div>
);

View File

@ -1,11 +1,12 @@
import React from 'react';
import ForgetPasswordForm from '../ForgetPasswordForm';
import { useTranslation } from 'react-i18next';
const ForgetPassword = () => {
const { t } = useTranslation('auth');
return (
<div className="text-center">
<h5 className="mb-0"> Forgot your password?</h5>
<small>Enter your email and we'll send you a reset link.</small>
<h5 className="mb-0">{ t('forgotForm.title')}</h5>
<small>{ t('forgotForm.description')}</small>
<ForgetPasswordForm />
</div>
);

View File

@ -5,20 +5,23 @@ import Login from './Login';
// import Start from './Start';
// import Logout from './Logout';
// import Registration from './Registration';
// import ForgetPassword from './ForgetPassword';
// import PasswordReset from './PasswordReset';
// import ConfirmMail from './ConfirmMail';
import ForgetPassword from './ForgetPassword';
import PasswordReset from './PasswordReset';
import ConfirmMail from './ConfirmMail';
// import LockScreen from './LockScreen';
const AuthBasicRoutes = ({ match: { url } }) => (
<Switch>
<Route path={`${url}/login`} exact component={Login} />
<Route path={`${url}/forget-password`} exact component={ForgetPassword} />
<Route path={`${url}/confirm-mail`} exact component={ConfirmMail} />
<Route path={`${url}/reset_password_token`} exact component={PasswordReset} />
{/* <Route path={`${url}/start`} exact component={Start} />
<Route path={`${url}/logout`} exact component={Logout} />
<Route path={`${url}/register`} exact component={Registration} />
<Route path={`${url}/forget-password`} exact component={ForgetPassword} />
<Route path={`${url}/confirm-mail`} exact component={ConfirmMail} />
<Route path={`${url}/password-reset`} exact component={PasswordReset} />
<Route path={`${url}/lock-screen`} exact component={LockScreen} /> */}
{/*Redirect*/}

View File

@ -2,28 +2,27 @@ import React, { Fragment } from 'react';
import { Col, Row } from 'reactstrap';
import { Link } from 'react-router-dom';
import LoginForm from '../LoginForm';
import { useTranslation } from "react-i18next";
import { useTranslation } from 'react-i18next';
//const {t} = useTranslation();
const Login = () => {
const { t } = useTranslation('auth');
const Login = () => (
<Fragment>
<Row className="text-left justify-content-between">
<Col xs="auto">
<h5>Sign in</h5>
</Col>
<Col xs="auto">
<p className="fs--1 text-600">
or {' '}
{/* <Link to="/authentication/basic/register">create an account</Link> */}
<a href={`${process.env.REACT_APP_CLIENT_BASE_URL}/signup`}>Sign up</a>
</p>
</Col>
</Row>
<LoginForm />
</Fragment>
);
return (
<Fragment>
<Row className="text-left justify-content-between">
<Col xs="auto">
<h5>{t('signin')}</h5>
</Col>
<Col xs="auto">
<p className="fs--1 text-600">
or {/* <Link to="/authentication/basic/register">create an account</Link> */}
<a href={`${process.env.REACT_APP_CLIENT_BASE_URL}/signup`}>{t('signup')}</a>
</p>
</Col>
</Row>
<LoginForm />
</Fragment>
);
};
export default Login;

View File

@ -1,11 +1,16 @@
import React from 'react';
import PasswordResetForm from '../PasswordResetForm';
import { useTranslation } from 'react-i18next';
const PasswordReset = () => (
<div className="text-center">
<h5>Reset new password</h5>
<PasswordResetForm />
</div>
);
const PasswordReset = () => {
const { t } = useTranslation('auth');
return (
<div className="text-center">
<h5>{ t('resetForm.title')}</h5>
<small>{t('resetForm.subTitle')}</small>
<PasswordResetForm />
</div>
);
};
export default PasswordReset;

View File

@ -33,6 +33,7 @@ import JKMyFriends from '../page/JKMyFriends';
import JKNotifications from '../page/JKNotifications';
import JKMessageModal from '../profile/JKMessageModal';
import JKUnsubscribe from '../page/JKUnsubscribe';
import JKAppLaunch from '../page/JKAppLaunch';
import JKMusicSessions from '../page/JKMusicSessions';
import JKNewMusicSession from '../page/JKNewMusicSession';
@ -305,6 +306,7 @@ function JKDashboardMain() {
<PrivateRoute path="/shopping-cart" component={JKShoppingCart} />
<PrivateRoute path="/checkout/success" component={JKCheckoutSuccess} />
<PrivateRoute path="/checkout" component={JKCheckout} />
<PrivateRoute path="/applaunch" component={JKAppLaunch} />
{/*Redirect*/}
<Redirect to="/errors/404" />
</Switch>

View File

@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useResponsive } from '@farfetch/react-context-responsive';
import { useParams } from 'react-router-dom';
import { useParams, Link } from 'react-router-dom';
import { Card, CardBody, Row, Col } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { getUserDetail, postUserEvent, userOpenedJamTrackWebPlayer } from '../../helpers/rest';
@ -74,14 +74,39 @@ const JKJamTrack = () => {
<CardBody className="pt-3">{jamTrack && <JKMyJamTrackMixes />}</CardBody>
</Card>
</Col>
<Col sm={12} md={4} className={ greaterThan.sm ? null : 'mt-4' }>
<Col sm={12} md={4}>
<Card className="mx-auto">
<FalconCardHeader title={t('jamtrack.create_mix.title')} titleClass="font-weight-semi-bold" />
<CardBody className="pt-3">{jamTrack && <JKCreateCustomMix />}</CardBody>
</Card>
</Col>
<Col />
<Col sm={12} md={4}>
<Card className="mx-auto">
<FalconCardHeader title={t('jamtrack.help_resources.title')} titleClass="font-weight-semi-bold" />
<CardBody className="pt-3">
<div className='mb-3'>
<div><a target='_blank' href="https://jamkazam.freshdesk.com/support/solutions/articles/66000501472">{t('jamtrack.help_resources.using_overview.title')}</a></div>
{t('jamtrack.help_resources.using_overview.description')}
</div>
<div className='mb-3'>
<div><a target='_blank' href="https://jamkazam.freshdesk.com/support/solutions/articles/66000125792">{t('jamtrack.help_resources.using_mac_windows.title')}</a></div>
{t('jamtrack.help_resources.using_mac_windows.description')}
</div>
<div className='mb-3'>
<div>
<Link to="/my-jamtracks">{t('jamtrack.help_resources.jamtracks_home.title')}</Link>
</div>
{t('jamtrack.help_resources.jamtracks_home.description')}
</div>
<div className='mb-3'>
<div>
<Link to="/jamtracks">{t('jamtrack.help_resources.see_more.title')}</Link>
</div>
{t('jamtrack.help_resources.see_more.description')}
</div>
</CardBody>
</Card>
</Col>
</Row>
) : null}
</>

View File

@ -1,8 +1,10 @@
import React, { useState } from 'react';
import { Row, Col } from 'reactstrap';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
const JKJamTrackArtists = ({ artists, showArtists, onSelect }) => {
const { t } = useTranslation('jamtracks');
const [expanded, setExpanded] = useState(false);
const handleClick = artist => {
onSelect(artist);
@ -18,7 +20,7 @@ const JKJamTrackArtists = ({ artists, showArtists, onSelect }) => {
<>
<Row>
<Col>
<strong>Search Results: Artists</strong>
<strong>{t('search.search_results.artists')}</strong>
</Col>
</Row>
{artists.length > 0 ? (
@ -43,7 +45,7 @@ const JKJamTrackArtists = ({ artists, showArtists, onSelect }) => {
<Row className='mt-1 mb-1'>
<Col>
<a href="#" onClick={toggleMoreArtists}>
{expanded ? 'Show fewer artists' : 'Show all artists'}
{expanded ? t('search.search_results.show_fewer_artists') : t('search.search_results.show_more_artists')}
</a>
</Col>
</Row>
@ -51,7 +53,7 @@ const JKJamTrackArtists = ({ artists, showArtists, onSelect }) => {
</>
) : (
<Row className="mb-2">
<Col>No matching artists</Col>
<Col>{t('search.search_results.no_matching_artists')}</Col>
</Row>
)}
</>

View File

@ -65,6 +65,7 @@ const JKJamTrackPlayer = () => {
const audioUrl =
process.env.REACT_APP_API_BASE_URL +
`/mixdowns/${activeMix.id}/download.mp3?file_type=mp3&sample_rate=48&mark=${result.visitorId}`;
console.log('audioUrl', audioUrl);
return audioUrl;
};

View File

@ -1,16 +1,20 @@
import React from 'react';
import { Button } from 'reactstrap';
import PropTypes from 'prop-types';
import { addJamtrackToShoppingCart } from '../../helpers/rest';
import { useHistory } from 'react-router-dom';
import { useAuth } from '../../context/UserAuth';
import { toast } from 'react-toastify';
import { useShoppingCart } from '../../hooks/useShoppingCart';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import JKTooltip from '../common/JKTooltip';
const JKJamTrackPurchaseButton = ({ jamTrack }) => {
const history = useHistory();
const { currentUser } = useAuth();
const { addCartItem } = useShoppingCart();
const { t } = useTranslation('jamtracks');
const addToCart = async () => {
const options = {
@ -18,11 +22,11 @@ const JKJamTrackPurchaseButton = ({ jamTrack }) => {
variant: 'full'
};
if (await addCartItem(options)) {
toast.success('JamTrack added to cart');
toast.success(t('search.list.add_success_alert'));
history.push('/shopping-cart');
} else {
console.log('Add to Cart Error');
toast.error('Error adding to cart');
toast.error(t('search.list.add_error_alert'));
}
};
@ -30,23 +34,22 @@ const JKJamTrackPurchaseButton = ({ jamTrack }) => {
<>
{jamTrack.purchaed ? (
<Button color="light" size="sm" className="mr-1">
Purchased
{t('search.list.purchased')}
</Button>
) : jamTrack.is_free && currentUser && currentUser.show_free_jamtrack ? (
<Button color="primary" onClick={addToCart} size="sm" className="mr-1">
Get It Free!
</Button>
) : jamTrack.added_cart ? (
) : jamTrack.allow_free && currentUser && currentUser.show_free_jamtrack ? (
<>
<Button color="light" size="sm" className="mr-1">
Already in Cart
<Button color="primary" onClick={addToCart} size="sm" className="mr-1">
{t('search.list.get_it_free')}
</Button>
<JKTooltip title={t('search.list.get_it_free_help_text')} />
</>
) : jamTrack.added_cart ? (
<Link to="/shopping-cart">{t('search.list.already_in_cart')}</Link>
) : (
<>
<div className="fs-1">$ {jamTrack.download_price}</div>
<Button color="primary" size="sm" className="mr-1" onClick={addToCart}>
Add to Cart
{t('search.list.add_to_cart')}
</Button>
</>
)}

View File

@ -111,7 +111,7 @@ const JKJamTrackTrack = ({ track }) => {
</span>
{trackInfo && (
<>
<span className="mr-1">
<span className="mr-1 pb-1">
<JKInstrumentIcon instrumentId={trackInfo.instrumentId} instrumentName={trackInfo.instrumentDescription} />
</span>
<span>

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Card, CardBody, Row, Col } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
@ -6,6 +7,9 @@ import JKJamTracksAutoComplete from './JKJamTracksAutoComplete';
import { getJamTracks, getJamTrackArtists, autocompleteJamTracks } from '../../helpers/rest';
import JKJamTrackArtists from './JKJamTrackArtists';
import JKJamTracksList from './JKJamTracksList';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useShoppingCart } from '../../hooks/useShoppingCart';
import { useResponsive } from '@farfetch/react-context-responsive';
const JKJamTracksFilter = () => {
const { t } = useTranslation('jamtracks');
@ -19,6 +23,12 @@ const JKJamTracksFilter = () => {
const [autoCompleteInputValue, setAutoCompleteInputValue] = useState('');
const [page, setPage] = useState(1);
const PER_PAGE = 10;
const { shoppingCart } = useShoppingCart();
const { greaterThan } = useResponsive();
// useEffect(() => {
// setCartItems(shoppingCart);
// }, []);
useEffect(() => {
if (selected) {
@ -46,7 +56,7 @@ const JKJamTracksFilter = () => {
return options;
};
const handleOnSelect = async (selected) => {
const handleOnSelect = async selected => {
setPage(1);
setArtists([]);
setJamTracks([]);
@ -57,7 +67,7 @@ const JKJamTracksFilter = () => {
await fetchJamTracks(params);
};
const handleOnEnter = async(queryStr) => {
const handleOnEnter = async queryStr => {
setPage(1);
setArtists([]);
setJamTracks([]);
@ -69,7 +79,7 @@ const JKJamTracksFilter = () => {
await fetchJamTracks(params);
};
const handleOnSelectArtist = async(artist) => {
const handleOnSelectArtist = async artist => {
setPage(1);
const selectedOpt = {
type: 'artist',
@ -84,7 +94,7 @@ const JKJamTracksFilter = () => {
const currentQuery = selected ? selected : searchTerm;
const params = queryOptions(currentQuery);
await fetchJamTracks(params);
}
};
// const fetchJamTracks = options => {
// getJamTracks(options)
@ -102,8 +112,7 @@ const JKJamTracksFilter = () => {
// });
// };
const fetchJamTracks = async(options) => {
const fetchJamTracks = async options => {
try {
console.log('fetchJamTracks', options);
const resp = await getJamTracks(options);
@ -111,12 +120,10 @@ const JKJamTracksFilter = () => {
console.log('jamtracks', data);
setJamTracks(prev => [...prev, ...data.jamtracks]);
setNextOffset(data.next);
} catch (error) {
console.error('error', error);
}
}
};
const fetchArtists = query => {
const options = {
@ -142,6 +149,11 @@ const JKJamTracksFilter = () => {
<Card>
<FalconCardHeader title={t('search.page_title')} titleClass="font-weight-bold" />
<CardBody className="pt-3">
{!greaterThan.sm && (
<Row>
<JKJamTrackFilterLinks shoppingCart={shoppingCart} wrapperClassNames='d-flex flex-column' shoppingCartClassNames="col mt-1 mb-2" downloadLinkClassNames='col' />
</Row>
)}
<Row>
<Col>
<JKJamTracksAutoComplete
@ -155,14 +167,15 @@ const JKJamTracksFilter = () => {
inputPlaceholder={t('search.search_input.placeholder')}
/>
</Col>
<Col className="text-right">
<span>
Download JamTracks catalog as a{' '}
<a data-testid="download-pdf" href="https://s3.amazonaws.com/jamkazam-public/public/lists/all-jamkazam-jamtracks.pdf">
<strong>PDF file</strong>
</a>
</span>
</Col>
{greaterThan.sm && (
<Col>
<JKJamTrackFilterLinks
shoppingCart={shoppingCart}
wrapperClassNames="d-flex justify-content-end"
shoppingCartClassNames="ml-3 mr-1"
/>
</Col>
)}
</Row>
<div className="mb-3">
@ -173,10 +186,40 @@ const JKJamTracksFilter = () => {
showArtists={showArtists}
/>
</div>
<JKJamTracksList selectedType={selected?.type} searchTerm={searchTerm} jamTracks={jamTracks} nextOffset={nextOffset} onNextPage={handleOnNextJamTracksPage} />
<JKJamTracksList
selectedType={selected?.type}
searchTerm={searchTerm}
jamTracks={jamTracks}
nextOffset={nextOffset}
onNextPage={handleOnNextJamTracksPage}
/>
</CardBody>
</Card>
);
};
const JKJamTrackFilterLinks = ({ shoppingCart, wrapperClassNames, shoppingCartClassNames, downloadLinkClassNames }) => {
return (
<div className={wrapperClassNames}>
<div className={downloadLinkClassNames}>
Download JamTracks catalog as a{' '}
<a
data-testid="download-pdf"
href="https://s3.amazonaws.com/jamkazam-public/public/lists/all-jamkazam-jamtracks.pdf"
>
<strong>PDF file</strong>
</a>
</div>
{shoppingCart.length > 0 && (
<div className={shoppingCartClassNames}>
<Link to="shopping-cart" className="btn btn-primary btn-sm">
<FontAwesomeIcon icon="shopping-cart" className="mr-1" />
View Cart ({shoppingCart.length})
</Link>
</div>
)}
</div>
);
};
export default JKJamTracksFilter;

View File

@ -4,15 +4,17 @@ import JKJamTrackPreview from './JKJamTrackPreview';
import JKJamTrackPurchaseButton from './JKJamTrackPurchaseButton';
import { JamTrackPreviewProvider } from '../../context/JamTrackPreviewContext';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
const JKJamTracksList = ({ selectedType, searchTerm, jamTracks, nextOffset, onNextPage }) => {
const { t } = useTranslation('jamtracks');
return (
<>
{selectedType && searchTerm.length && jamTracks.length > 0 ? (
<Row className="mb-2">
<Col>
<strong>
Search Results: JamTracks for {selectedType} "{searchTerm}"
{t('search.search_results.for_selection')} {selectedType} "{searchTerm}"
</strong>
</Col>
</Row>
@ -21,7 +23,7 @@ const JKJamTracksList = ({ selectedType, searchTerm, jamTracks, nextOffset, onNe
jamTracks.length > 0 && (
<Row className="mb-2">
<Col>
<strong>Search Results: JamTracks including "{searchTerm}"</strong>
<strong>{t('search.search_results.for_search_term')} "{searchTerm}"</strong>
</Col>
</Row>
)
@ -32,22 +34,22 @@ const JKJamTracksList = ({ selectedType, searchTerm, jamTracks, nextOffset, onNe
<Table striped bordered className="fs--1" data-testid="jamtracks-table">
<thead className="bg-200 text-900">
<tr>
<th width="30%">Song</th>
<th width="55%">Tracks</th>
<th>Shop</th>
<th width="30%">{t('search.list.song')}</th>
<th width="55%">{t('search.list.tracks')}</th>
<th>{t('search.list.shop')}</th>
</tr>
</thead>
<tbody>
<JamTrackPreviewProvider>
{jamTracks.map((jamTrack, index) => (
<tr key={`jamtrck-preview-row-${jamTrack.id}`}>
<td className='track-name-col'>
<td className="track-name-col">
{jamTrack.name} by {jamTrack.original_artist}
</td>
<td className='track-tracks-col'>
<td className="track-tracks-col">
<JKJamTrackPreview jamTrack={jamTrack} />
</td>
<td className='purchase-button-col'>
<td className="purchase-button-col">
<JKJamTrackPurchaseButton jamTrack={jamTrack} />
</td>
</tr>
@ -62,7 +64,7 @@ const JKJamTracksList = ({ selectedType, searchTerm, jamTracks, nextOffset, onNe
<Row>
<Col>
<Button color="primary" onClick={onNextPage} data-testid="moreBtn">
Load More
{t('search.list.load_more')}
</Button>
</Col>
</Row>
@ -76,7 +78,7 @@ JKJamTracksList.propTypes = {
searchTerm: PropTypes.string,
jamTracks: PropTypes.array,
nextOffset: PropTypes.number,
onNextPage: PropTypes.func,
onNextPage: PropTypes.func
};
JKJamTracksList.defaultProps = {

View File

@ -0,0 +1,50 @@
import React, { useEffect, useState } from 'react';
import jkCustomUrlScheme from '../../helpers/jkCustomUrlScheme';
import { useTranslation } from 'react-i18next';
import { Card, CardBody, Row, Col } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
const JKCustomUrlSchemaHandle = () => {
const [urlScheme, setUrlScheme] = useState(null);
const { t } = useTranslation();
useEffect(() => {
const queryStr = window.location.search;
if (!queryStr) return;
const urlParams = new URLSearchParams(queryStr);
const action = urlParams.get('act');
const params = urlParams.get('p');
const appUrl = jkCustomUrlScheme(action, params);
setUrlScheme(appUrl);
}, []);
useEffect(() => {
if (urlScheme) {
console.log('opening custom url', urlScheme);
window.open(urlScheme, '_self');
}
}, [urlScheme]);
return (
<div>
<Card>
<FalconCardHeader title={t('new.page_title', { ns: 'sessions' })} titleClass="font-weight-bold" />
<CardBody className="pt-0">
<Row>
<Col>
<div className='pt-5 text-center pb-5'>
{urlScheme && (
<p>
If JamKazam app doesn't open automatically, <a href={urlScheme}>click here</a>.
</p>
)}
</div>
</Col>
</Row>
</CardBody>
</Card>
</div>
);
};
export default JKCustomUrlSchemaHandle;

View File

@ -1,5 +1,5 @@
import React, { useRef, useState, useEffect } from 'react';
//import {useHistory} from 'react-router-dom';
import {useHistory} from 'react-router-dom';
import { Form, FormGroup, Input, Label, Card, CardBody, Button, Row, Col } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import JKTooltip from '../common/JKTooltip';
@ -8,18 +8,11 @@ import { useAuth } from '../../context/UserAuth';
import JKFriendsAutoComplete from '../people/JKFriendsAutoComplete';
import JKSessionInviteesChips from '../people/JKSessionInviteesChips';
import { getFriends } from '../../helpers/rest';
import jkCustomUrlScheme from '../../helpers/jkCustomUrlScheme';
import JKModalDialog from '../common/JKModalDialog';
import useNativeAppCheck from '../../hooks/useNativeAppCheck';
import { useNativeApp } from '../../context/NativeAppContext';
import { useResponsive } from '@farfetch/react-context-responsive';
import { sessionPrivacyMap } from '../../config';
// const privacyMap = {
// public: 1,
// private_invite: 2,
// private_approve: 3
// };
const JKNewMusicSession = () => {
const { currentUser } = useAuth();
@ -31,7 +24,7 @@ const JKNewMusicSession = () => {
const [privacy, setPrivacy] = useState('1');
const [submitted, setSubmitted] = useState(false);
const [showAppUnavailable, setShowAppUnavailable] = useState(false);
//const history = useHistory();
const history = useHistory();
const formRef = useRef();
const isNativeAppAvailable = useNativeAppCheck();
const { nativeAppUnavailable, setNativeAppUnavailable } = useNativeApp();
@ -91,15 +84,11 @@ const JKNewMusicSession = () => {
} catch (error) {
console.error('localStorage is not available', error);
}
//check if jamkazam app is installed
try {
//check if jamkazam app is installed
await isNativeAppAvailable();
//window.open jamkazam app url using custom URL scheme
//an example URL would be: jamkazam://url=https://www.jamkazam.com/client#/createSession/privacy~2|description~hello|inviteeIds~1,2,3,4
const q = `privacy~${payload.privacy}|description~${payload.description}|inviteeIds~${payload.inviteeIds}`;
const urlScheme = jkCustomUrlScheme('createSession', q);
window.location.href = urlScheme;
//history.push('/sessions');
history.push(`/applaunch?act=createSession&p=${q}`);
} catch (error) {
toggleAppUnavilableModel();
}

View File

@ -7,16 +7,13 @@ import { useTranslation } from 'react-i18next';
import { useResponsive } from '@farfetch/react-context-responsive';
import PropTypes from 'prop-types';
import { Row, Col, Button, UncontrolledTooltip } from 'reactstrap';
import jkCustomUrlScheme from '../../helpers/jkCustomUrlScheme';
import JKUserLatencyBadge from '../profile/JKUserLatencyBadge';
import JKSessionUser from './JKSessionUser';
import useNativeAppCheck from '../../hooks/useNativeAppCheck';
import { useNativeApp } from '../../context/NativeAppContext';
import EnterIcon from '../../icons/enter.svg';
import JKInstrumentIcon from '../profile/JKInstrumentIcon';
import {useHistory} from 'react-router-dom';
import useSessionHelper from './JKUseSessionHelper';
function JKSession({ session }) {
@ -161,6 +158,7 @@ function JoinSessionButton({ session }) {
const isNativeAppAvailable = useNativeAppCheck();
const { setNativeAppUnavailable } = useNativeApp();
const { t } = useTranslation();
const history = useHistory();
async function joinSession(e) {
e.preventDefault();
@ -169,9 +167,8 @@ function JoinSessionButton({ session }) {
} else {
try {
await isNativeAppAvailable();
const q = `joinSessionId~${session.id}`;
const urlScheme = jkCustomUrlScheme('findSession', q);
window.document.href = urlScheme;
const q = `sessionId~${session.id}`;
history.push(`/applaunch?act=joinSession&p=${q}`);
return;
} catch (error) {
setNativeAppUnavailable(true);

View File

@ -7,9 +7,12 @@ import JKLatencyBadge from '../profile/JKLatencyBadge';
import JKProfileSidePanel from '../profile/JKProfileSidePanel';
import JKProfileAvatar from '../profile/JKProfileAvatar';
import PropTypes from 'prop-types';
import { useAuth } from '../../context/UserAuth';
function JKSessionUser({ user }) {
const dispatch = useDispatch();
const { currentUser } = useAuth();
const latencyData = useSelector(state => {
const userLatency = state.latency.latencies.find(l => l.user_id === user.id);
return {
@ -62,7 +65,8 @@ function JKSessionUser({ user }) {
</a>
</div>
<div className="ml-2 ms-2" style={{ marginRight: 'auto' }}>
<JKLatencyBadge latencyData={latencyData} showBadgeOnly={true} />
{ currentUser && currentUser.id !== user.id && <JKLatencyBadge latencyData={latencyData} showBadgeOnly={true} /> }
&nbsp;
</div>
</div>
) : (

View File

@ -8,6 +8,7 @@ import JKUserLatencyBadge from '../profile/JKUserLatencyBadge';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/UserAuth';
import { fetchUserLatencies } from '../../store/features/latencySlice';
import { useDispatch } from 'react-redux';
const JKSessionsHistoryItem = ({ session_id, sessionGroup }) => {
const { greaterThan } = useResponsive();
@ -15,6 +16,7 @@ const JKSessionsHistoryItem = ({ session_id, sessionGroup }) => {
const [participants, setParticipants] = useState([]);
const { t } = useTranslation();
const { currentUser } = useAuth();
const dispatch = useDispatch();
useEffect(() => {
console.log('sessionGroup', sessionGroup);
@ -30,8 +32,8 @@ const JKSessionsHistoryItem = ({ session_id, sessionGroup }) => {
name: `${history.first_name} ${history.last_name}`
}
};
if (history.instrments) {
participant.tracks = history.instrments.split('|').map((instrument, index) => ({
if (history.instruments) {
participant.tracks = history.instruments.split('|').map((instrument, index) => ({
id: index,
instrument_id: instrument,
instrument: instrument
@ -43,14 +45,11 @@ const JKSessionsHistoryItem = ({ session_id, sessionGroup }) => {
);
}, [sessionGroup]);
useEffect(() => {
if (participants.length > 0 && currentUser) {
const currentUserId = currentUser.id;
const otherUserIds = participants.filter(p => p.id !== currentUserId).map(p => p.id);
console.log('X_DEBUG_ JKSessionsHistoryItem', currentUserId, otherUserIds);
fetchUserLatencies({ currentUserId, otherUserIds});
dispatch(fetchUserLatencies({ currentUserId, otherUserIds }));
}
}, [participants]);
@ -94,10 +93,14 @@ const JKSessionsHistoryItem = ({ session_id, sessionGroup }) => {
))}
</td>
<td className="text-center">
{participants.filter(p => p.user.id !== currentUser.id).map(participant => (
{participants.map(participant => (
<Row key={participant.id} style={musicianRowStyle}>
<Col>
<JKUserLatencyBadge key={participant.id} user={participant.user} showBadgeOnly={true} />
{participant.id !== currentUser.id ? (
<JKUserLatencyBadge key={participant.id} user={participant.user} showBadgeOnly={true} />
) : (
<span />
)}
</Col>
</Row>
))}
@ -106,22 +109,23 @@ const JKSessionsHistoryItem = ({ session_id, sessionGroup }) => {
{participants.map(participant => (
<Row style={musicianRowStyle} key={participant.id} data-testid={`Participant${participant.id}Tracks`}>
<Col>
{participant.tracks && participant.tracks.map(track => (
<span key={track.id} className="mr-1 mb-1" title={track.instrment}>
<a
id={`Participant${participant.id}Track${track.id}Instrument`}
data-testid={`Track${track.id}Instrument`}
>
<JKInstrumentIcon instrumentId={track.instrument_id} instrumentName={track.instrument} />
</a>
<UncontrolledTooltip
placement="top"
target={`Participant${participant.id}Track${track.id}Instrument`}
>
{track.instrument}
</UncontrolledTooltip>
</span>
))}
{participant.tracks &&
participant.tracks.map(track => (
<span key={track.id} className="mr-1 mb-1" title={track.instrment}>
<a
id={`Participant${participant.id}Track${track.id}Instrument`}
data-testid={`Track${track.id}Instrument`}
>
<JKInstrumentIcon instrumentId={track.instrument_id} instrumentName={track.instrument} />
</a>
<UncontrolledTooltip
placement="top"
target={`Participant${participant.id}Track${track.id}Instrument`}
>
{track.instrument}
</UncontrolledTooltip>
</span>
))}
</Col>
</Row>
))}

View File

@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next';
import moment from 'moment';
const useSessionHelper = (session) => {
const { t } = useTranslation();
@ -16,19 +17,9 @@ const useSessionHelper = (session) => {
};
const sessionDateTime = session => {
if(!session.created_at) return '';
const date = new Date(session.created_at);
const d = new Date(date);
// return d.toLocaleDateString('en-us', {
// weekday: 'long',
// year: 'numeric',
// month: 'short',
// day: 'numeric',
// timeZoneName: 'short'
// });
return new Intl.DateTimeFormat('en-US', {
dateStyle: 'full',
timeStyle: 'long',
}).format(date);
return moment(date).format('dddd, MMMM DD, YYYY, h:mmA z');
};
return {

View File

@ -1,5 +1,9 @@
export default (section, queryStr) => {
const url = encodeURI(`${process.env.REACT_APP_CLIENT_BASE_URL}/signin?redirect-to=/client#/${section}/custom~yes|${queryStr}`);
const urlScheme = `jamkazam://url=${url}`;
//remove the leading '?' from queryStr
if(queryStr.startsWith('?')) {
queryStr = queryStr.substring(1);
}
const url = encodeURI(`${process.env.REACT_APP_CLIENT_BASE_URL}/client#/${section}/custom~yes|${queryStr}`);
const urlScheme = `jamkazam://${url}`;
return urlScheme;
};

View File

@ -298,6 +298,39 @@ export const requestPasswordReset = userId => {
});
};
export const resetPassword = email => {
return new Promise((resolve, reject) => {
apiFetch(`/users/reset_password`, {
method: 'POST',
body: JSON.stringify({ email })
})
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const requstResetForgotPassword = email => {
return new Promise((resolve, reject) => {
apiFetch(`/request_reset_forgot_password`, {
method: 'POST',
body: JSON.stringify({ email })
})
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const resetForgotPassword = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/reset_forgot_password`, {
method: 'POST',
body: JSON.stringify(options)
})
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const postUserAppInteraction = (userId, options) => {
return new Promise((resolve, reject) => {
apiFetch(`/users/${userId}/app_interactions`, {

View File

@ -68,7 +68,9 @@
},
"signups": {
"page_title": "Signups",
"no_data": "No signups yet",
"no_signup_data": "There is no signup data yet for your affiliate account. To start to generate affiliate signups, please ",
"share_affiliate_links": "share affiliate links",
"with_your_audience": " with your audience.",
"load_more": "Load More",
"list": {
"header": {
@ -80,7 +82,9 @@
},
"earnings": {
"page_title": "Earnings",
"no_data": "No earnings yet",
"no_earnings_data": "There is no earnings data yet for your affiliate account. To start to generate affiliate signups, please ",
"share_affiliate_links": "share affiliate links",
"start_to_earn": " with your audience. Once you start to get signups, there is typically some lag time until new users convert to premium subscribers, generating affiliate income (e.g. new users get a free 30-day gold plan at signup).",
"load_more": "Load More",
"list": {
"header": {

View File

@ -1,5 +1,36 @@
{
"signup": "Sign Up",
"signin": "Sign In",
"signout": "Sign Out"
"signout": "Sign Out",
"signinForm": {
"email": "Email Address",
"password": "Password",
"remember": "Remember me",
"forgot": "Forgot password?",
"submit": "Sign In"
},
"forgotForm": {
"title": "Forgot Your Password",
"description": "Enter your email address and we'll send you a link to reset your password.",
"email": "Email Address",
"submit": "Send Reset Link",
"submitting": "Sending Reset Link..."
},
"confirmEmailContent": {
"title": "Please check your email!",
"description_1": "An email has been sent to ",
"toYou": "to you",
"description_2": "Please click on the included link to reset your password.",
"description_3": "If you don't see the email, check other places it might be, like your junk, spam, social, or other folders. If you still can't find it, you may not be entering the email address that you used when signing up with the service.",
"returnToLogin": "Return to Sign In"
},
"resetForm": {
"title": "Reset Your Password",
"subTitle": "Enter your new password below.",
"password": "New Password",
"confirmPassword": "Confirm New Password",
"submit": "Reset Password",
"submitting": "Resetting Password...",
"successMessage": "Your password has been reset. Please sign in with your new password."
}
}

View File

@ -4,6 +4,27 @@
"search_input": {
"title": "Search",
"placeholder": "Search by artist, song, style, or keyword"
},
"search_results": {
"artists": "Search Results: Artists",
"show_fewer_artists": "Show Fewer Artists",
"show_more_artists": "Show More Artists",
"no_matching_artists": "No matching artists found.",
"for_selection": "Search Results: JamTracks for",
"for_search_term": "Search Results: JamTracks including"
},
"list": {
"song": "Song",
"tracks": "Tracks",
"shop": "Shop",
"load_more": "Load More",
"get_it_free_help_text": "Click this button to get your first JamTrack free.",
"purchased": "Purchased",
"get_it_free": "Get it Free!",
"add_to_cart": "Add to Cart",
"already_in_cart": "Already in Cart",
"add_success_alert": "JamTrack was added to cart successfully.",
"add_error_alert": "There was an error adding the JamTrack to the cart."
}
},
"my": {
@ -39,6 +60,26 @@
"pitch": "Pitch",
"mix_name": "Mix Name",
"create": "Create Mix"
},
"help_resources": {
"title": "Helpful Resources",
"using_overview": {
"title": "Using JamTracks - Overview ",
"description": "that explains how to use JamTracks in the browser."
},
"using_mac_windows": {
"title": "Using JamTracks in the Mac or Windows App",
"description": "that explains how to use JamTracks in the JamKazam app."
},
"jamtracks_home": {
"title": "Go to the JamTracks Home Page",
"description": "where you can access all of your JamTracks and search for more."
},
"see_more": {
"title": "see more JamTracks by this artist",
"description": "to checkout more songs you might like."
}
}
}
}

View File

@ -4,16 +4,19 @@ import Logo from '../components/navbar/Logo';
import Section from '../components/common/Section';
import AuthBasicRoutes from '../components/auth/basic/JKAuthBasicRoutes';
import UserAuth from '../context/UserAuth';
import { BrowserQueryProvider } from '../context/BrowserQuery';
const AuthBasicLayout = ({location}) => (
const AuthBasicLayout = ({ location }) => (
<Section className="py-0">
<Row className="flex-center min-vh-100 py-6">
<Col sm={10} md={8} lg={6} xl={5} className="col-xxl-4">
<Logo width={200} />
<Card>
<CardBody className="fs--1 font-weight-normal p-5">
<UserAuth path={location.pathname}>
<AuthBasicRoutes />
<UserAuth path={location.pathname}>
<BrowserQueryProvider>
<AuthBasicRoutes />
</BrowserQueryProvider>
</UserAuth>
</CardBody>
</Card>

View File

@ -69,11 +69,11 @@ export const affiliateRoutes = {
icon: 'dollar-sign',
children: [
{ to: '/affiliate/program', name: 'Program'},
{ to: '/affiliate/payee', name: 'Payee'},
{ to: '/affiliate/agreement', name: 'Agreement'},
{ to: '/affiliate/links', name: 'Links'},
{ to: '/affiliate/payee', name: 'Payee'},
{ to: '/affiliate/signups', name: 'Signups'},
{ to: '/affiliate/earnings', name: 'Earnings'},
{ to: '/affiliate/agreement', name: 'Agreement'}
]
}

View File

@ -27,6 +27,6 @@ module MailerHelper
latency_scores[:unknown][:label]
end
lbl
lbl.titleize
end
end

View File

@ -11,7 +11,7 @@
<div style="padding: 2em; margin-top: 2em; background-color: #f0f0f0;">
<div
style="
margin: 3em auto 3em auto;
margin: 3em auto 1.5em auto;
padding: 2em;
background-color: #ffffff;
max-width: 700px;
@ -25,39 +25,46 @@
</div>
<footer style="text-align: center; margin: 0px auto; padding: 0px auto;">
<% if @user && @user.unsubscribe_token -%>
<p>
<%= I18n.t "mailer_layout.footer.paragraph1" -%> <a href="https://www.jamkazam.com" target="_blank">JamKazam</a>. <a
href="https://www.jamkazam.com/unsubscribe/<%= @user.unsubscribe_token %>"
style="
color: #2c7be5;
text-decoration: none;
border-bottom: 1px solid #2c7be5;
"
><%= I18n.t "mailer_layout.footer.unsubscribe" -%></a>
</p>
<% end -%>
<div style="text-align: center; margin: 1em 0;">
<a href="https://www.facebook.com" target="_blank" style="text-decoration: none;">
<img src="<%= asset_url("/email/fb-icon.svg", host: APP_CONFIG.action_mailer.asset_host ) -%>" alt="Facebook" style="width: 24px; height: 24px; margin: 0 5px;">
</a>
<a href="https://www.instagram.com" target="_blank" style="text-decoration: none;">
<img src="<%= asset_url("/email/instagram-icon.svg", host: APP_CONFIG.action_mailer.asset_host ) -%>" alt="Instagram" style="width: 24px; height: 24px; margin: 0 5px;">
</a>
<a href="https://www.tiktok.com" target="_blank" style="text-decoration: none;">
<img src="<%= asset_url("/email/tiktok-icon.svg", host: APP_CONFIG.action_mailer.asset_host ) -%>" alt="TikTok" style="width: 24px; height: 24px; margin: 0 5px;">
</a>
<a href="https://www.youtube.com" target="_blank" style="text-decoration: none;">
<img src="<%= asset_url("/email/youtube-icon.svg", host: APP_CONFIG.action_mailer.asset_host ) -%>" alt="YouTube" style="width: 24px; height: 24px; margin: 0 5px;">
</a>
<a href="https://www.x.com" target="_blank" style="text-decoration: none;">
<img src=<%= asset_url("/email/twitter-x-icon.svg", host: APP_CONFIG.action_mailer.asset_host ) -%>" alt="X.com" style="width: 24px; height: 24px; margin: 0 5px;">
</a>
</div>
<p>
<%= I18n.t "mailer_layout.footer.copyright" -%>
</p>
<small>
<% if @user && @user.unsubscribe_token -%>
<p>
<%= I18n.t "mailer_layout.footer.paragraph1" -%> <a href="https://www.jamkazam.com" target="_blank">JamKazam</a>. <br /> <%= I18n.t "mailer_layout.footer.you_can" -%> <a
href="https://www.jamkazam.com/unsubscribe/<%= @user.unsubscribe_token %>"
style="
color: #2c7be5;
text-decoration: none;
border-bottom: 1px solid #2c7be5;
"
><%= I18n.t "mailer_layout.footer.unsubscribe" -%></a>
<%= I18n.t "mailer_layout.footer.to_stop_receiving_emails" -%>
</p>
<% end -%>
<div style="text-align: center; margin: 1em 0;">
<span style="margin-right: 1em;">
<%= I18n.t "mailer_layout.footer.connect_with_us" -%>
</span>
<a href="https://www.facebook.com" target="_blank" style="text-decoration: none;">
<img src="<%= asset_url("/email/fb-icon.svg", host: APP_CONFIG.action_mailer.asset_host ) -%>" alt="Facebook" style="width: 24px; height: 24px; margin: 0 5px;">
</a>
<a href="https://www.instagram.com" target="_blank" style="text-decoration: none;">
<img src="<%= asset_url("/email/instagram-icon.svg", host: APP_CONFIG.action_mailer.asset_host ) -%>" alt="Instagram" style="width: 24px; height: 24px; margin: 0 5px;">
</a>
<a href="https://www.tiktok.com" target="_blank" style="text-decoration: none;">
<img src="<%= asset_url("/email/tiktok-icon.svg", host: APP_CONFIG.action_mailer.asset_host ) -%>" alt="TikTok" style="width: 24px; height: 24px; margin: 0 5px;">
</a>
<a href="https://www.youtube.com" target="_blank" style="text-decoration: none;">
<img src="<%= asset_url("/email/youtube-icon.svg", host: APP_CONFIG.action_mailer.asset_host ) -%>" alt="YouTube" style="width: 24px; height: 24px; margin: 0 5px;">
</a>
<a href="https://www.x.com" target="_blank" style="text-decoration: none;">
<img src=<%= asset_url("/email/twitter-x-icon.svg", host: APP_CONFIG.action_mailer.asset_host ) -%>" alt="X.com" style="width: 24px; height: 24px; margin: 0 5px;">
</a>
</div>
<p>
<%= I18n.t "mailer_layout.footer.copyright" -%>
</p>
</small>
</footer>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -263,7 +263,6 @@
// called from sidebar when messages come in
function messageReceived(payload) {
alert("messageReceived");
if(showing && otherId == payload.sender_id) {
if(fullyInitialized) {
renderMessage(payload.msg, payload.sender_id, payload.sender_name, payload.created_at, true);

View File

@ -17,4 +17,26 @@ class ApiSessionsController < ApiController
render :json => {}, :status => :ok
end
end
#update password token. inteanded for the react app (spa)
def request_reset_password
begin
url = APP_CONFIG.spa_origin + '/authentication/basic'
User.reset_password(params[:email], url)
render :json => {}, :status => 204
rescue JamRuby::JamArgumentError
render :json => {:message => ValidationMessages::EMAIL_NOT_FOUND}, :status => 403
end
end
def reset_forgot_password
begin
User.set_password_from_token(params[:email], params[:token], params[:password], params[:password_confirmation])
render :json => {}, :status => 204
rescue JamRuby::JamArgumentError => e
render :json => {:message => e.field_message}, :status => 403
end
end
end

View File

@ -13,7 +13,7 @@ if !current_user
}
else
attributes :id, :name, :description, :musician_access, :approval_required, :friends_can_join, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active, :jam_track_initiator_id, :jam_track_id, :music_session_id_int, :use_video_conferencing_server
attributes :id, :name, :description, :musician_access, :approval_required, :friends_can_join, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active, :jam_track_initiator_id, :jam_track_id, :music_session_id_int, :use_video_conferencing_server, :created_at
if @on_join
node :subscription do |session|

View File

@ -1,3 +1,4 @@
object @cart
extends "api_shopping_carts/show"
node :show_free_jamtrack do

View File

@ -1,3 +1,5 @@
object @cart
extends "api_shopping_carts/show"
node :show_free_jamtrack do

View File

@ -9,8 +9,11 @@ en:
mailer_layout:
footer:
paragraph1: "This email was sent to you because you have an account at"
unsubscribe: "Unsubscribe"
you_can: "You can"
unsubscribe: "unsubscribe"
to_stop_receiving_emails: "to stop receiving emails from us."
copyright: "Copyright © 2024 JamKazam, Inc. All rights reserved."
connect_with_us: "Connect with us:"
user_mailer:
confirm_email:
subject: "Please confirm your JamKazam email address"
@ -30,7 +33,7 @@ en:
view_profile: "View Profile"
send_message: "Send Message"
send_friend_request: "Send Friend Request"
paragraph2: "To find great musical matches across the entire JamKazam commiunity and make new connections, use the button below to access our musician search feature. This let you filter JamKazammers by latency, instruments, skill level, genre interests, last active day and more."
paragraph2: "To find great musical matches across the entire JamKazam community and make new connections, use the button below to access our musician search feature. This lets you filter JamKazammers by latency, instruments, skill level, genre interests, last active date and more."
search_musicians: "Search JamKazam Musicians"
welcome_message:

View File

@ -9,7 +9,7 @@ en:
mailer_layout:
footer:
paragraph1: "This email was sent to you because you have an account at"
unsubscribe: "Unsubscribe"
unsubscribe: "unsubscribe"
copyright: "Copyright © 2024 JamKazam, Inc. All rights reserved."
user_mailer:
confirm_email:

View File

@ -16,9 +16,6 @@ Rails.application.routes.draw do
root to: 'users#home'
end
# signup, and signup completed, related pages
get '/signup', to: 'users#new'
post '/signup', to: 'users#create'
@ -32,6 +29,8 @@ Rails.application.routes.draw do
delete '/signout', to: 'sessions#destroy'
get '/passthrough', to: 'sessions#passthrough'
match '/redeem_giftcard', to: 'landings#redeem_giftcard', via: :get
match '/account/activate/code_old', to: 'landings#account_activate', via: :get
match '/account/activate/code', to: 'landings#amazon_lessons_promo_1', via: :get
@ -407,8 +406,13 @@ Rails.application.routes.draw do
match '/users/authorizations/google' => 'api_users#google_auth', :via => :get
match '/users/:id/set_password' => 'api_users#set_password', :via => :post
match '/users/:id/request_reset_password' => 'api_users#request_reset_password', :via => :post
match '/users/:id/reset_password' => 'api_users#reset_password', :via => :post
match '/users/:id/app_interactions' => 'api_users#post_app_interactions', :via => :post
#reset forgot password (not logged in)
post '/request_reset_forgot_password', to: 'api_sessions#request_reset_password'
post '/reset_forgot_password', to: 'api_sessions#reset_forgot_password'
match '/reviews' => 'api_reviews#index', :via => :get
match '/reviews' => 'api_reviews#create', :via => :post
match '/reviews/:id' => 'api_reviews#update', :via => :post