Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ workflows:
filters:
branches:
only:
- free
- thrive-vulnerability-1
# This is stage env for production QA releases
- "build-prod-staging":
context : org-global
Expand Down
8 changes: 6 additions & 2 deletions src/server/routes/contentful.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
*/

import express from 'express';

import { middleware } from 'tc-core-library-js';
import config from 'config';
import _ from 'lodash';
import {
ASSETS_DOMAIN,
IMAGES_DOMAIN,
Expand All @@ -14,6 +16,8 @@ import {

const cors = require('cors');

const authenticator = middleware.jwtAuthenticator;
const authenticatorOptions = _.pick(config.SECRET.JWT_AUTH, ['AUTH_SECRET', 'VALID_ISSUERS']);
const routes = express.Router();

// Enables CORS on those routes according config above
Expand Down Expand Up @@ -124,7 +128,7 @@ routes.use('/:spaceName/:environment/published/entries', (req, res, next) => {
});

/* Update votes on article. */
routes.use('/:spaceName/:environment/votes', (req, res, next) => {
routes.use('/:spaceName/:environment/votes', (req, res, next) => authenticator(authenticatorOptions)(req, res, next), (req, res, next) => {
articleVote(req.body)
.then(res.send.bind(res), next);
});
Expand Down
60 changes: 52 additions & 8 deletions src/shared/components/Contentful/Article/Article.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
import _ from 'lodash';
import React from 'react';
import { connect } from 'react-redux';
import PT from 'prop-types';
import { fixStyle } from 'utils/contentful';
import { getService } from 'services/contentful';
Expand All @@ -22,6 +23,8 @@ import {
config, Link, isomorphy,
} from 'topcoder-react-utils';
import qs from 'qs';
import LoginModal from 'components/LoginModal';
import modalStyle from 'components/LoginModal/modal.scss';
// SVGs and assets
import GestureIcon from 'assets/images/icon-gesture.svg';
import ReadMoreArrow from 'assets/images/read-more-arrow.svg';
Expand All @@ -41,21 +44,30 @@ const DEFAULT_BANNER_IMAGE = 'https://images.ctfassets.net/piwi0eufbb2g/7v2hlDsV
const RANDOM_BANNERS = ['6G8mjiTC1mzeSQ2YoUG1gB', '1DnDD02xX1liHfSTf5Vsn8', 'HQZ3mN0rR92CbNTkKTHJ5', '1OLoX8ZsvjAnn4TdGbZESD', '77jn01UGoQe2gqA7x0coQD'];
const RANDOM_BANNER = RANDOM_BANNERS[_.random(0, 4)];

export default class Article extends React.Component {
class Article extends React.Component {
componentDidMount() {
const { fields } = this.props;
this.setState({
upvotes: fields.upvotes || 0,
downvotes: fields.downvotes || 0,
showLogin: false,
voting: false,
});
}

// eslint-disable-next-line consistent-return
updateVote(type) {
let userVotes = localStorage.getItem(LOCAL_STORAGE_KEY);
userVotes = userVotes ? JSON.parse(userVotes) : {};
const {
id, spaceName, environment, preview,
id, spaceName, environment, preview, auth,
} = this.props;
// check for auth?
if (!auth) {
return this.setState({
showLogin: true,
});
}
let userVotes = localStorage.getItem(LOCAL_STORAGE_KEY);
userVotes = userVotes ? JSON.parse(userVotes) : {};
const articleVote = userVotes[id];
let { upvotes, downvotes } = this.state;
// Check if user alredy voted on this article?
Expand Down Expand Up @@ -93,17 +105,21 @@ export default class Article extends React.Component {
}
}
// Store user action
this.setState({
voting: true,
});
getService({ spaceName, environment, preview }).articleVote(id, {
upvotes,
downvotes,
})
}, auth.tokenV3)
.then(() => {
// Only when Contentful enntry was succesfully updated
// then we update the local store and the state
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(userVotes));
this.setState({
upvotes,
downvotes,
voting: false,
});
});
}
Expand All @@ -115,7 +131,9 @@ export default class Article extends React.Component {
const contentfulConfig = {
spaceName, environment, preview,
};
const { upvotes, downvotes } = this.state || {};
const {
upvotes, downvotes, showLogin, voting,
} = this.state || {};
let shareUrl;
if (isomorphy.isClientSide()) {
shareUrl = encodeURIComponent(window.location.href);
Expand Down Expand Up @@ -283,7 +301,7 @@ export default class Article extends React.Component {
{/* Voting */}
<div className={theme.actionContainer}>
<div className={theme.action}>
<div tabIndex={0} role="button" className={theme.circleGreenIcon} onClick={() => this.updateVote('up')} onKeyPress={() => this.updateVote('up')}>
<div tabIndex={0} role="button" className={voting ? theme.circleGreenIconDisabled : theme.circleGreenIcon} onClick={() => this.updateVote('up')} onKeyPress={() => this.updateVote('up')}>
<GestureIcon />
</div>
<span>
Expand All @@ -293,7 +311,7 @@ export default class Article extends React.Component {
</span>
</div>
<div className={theme.action}>
<div tabIndex={0} role="button" className={theme.circleRedIcon} onClick={() => this.updateVote('down')} onKeyPress={() => this.updateVote('down')}>
<div tabIndex={0} role="button" className={voting ? theme.circleRedIconDisabled : theme.circleRedIcon} onClick={() => this.updateVote('down')} onKeyPress={() => this.updateVote('down')}>
<GestureIcon />
</div>
<span>{downvotes}</span>
Expand Down Expand Up @@ -380,6 +398,19 @@ export default class Article extends React.Component {
) : null
}
</div>
{
showLogin && (
<LoginModal
// eslint-disable-next-line no-restricted-globals
retUrl={isomorphy.isClientSide() ? location.href : null}
onCancel={() => this.setState({ showLogin: false })}
modalTitle="Want to vote?"
modalText="You must be a Topcoder member to do that."
utmSource="thrive_article"
infoNode={<p className={modalStyle.regTxt}>Discover <a href="/community/learn" target="_blank" rel="noreferrer">other features</a> you can access by becoming a member.</p>}
/>
)
}
</React.Fragment>
);
}
Expand All @@ -388,6 +419,7 @@ export default class Article extends React.Component {
Article.defaultProps = {
spaceName: null,
environment: null,
auth: null,
};

Article.propTypes = {
Expand All @@ -398,4 +430,16 @@ Article.propTypes = {
preview: PT.bool.isRequired,
spaceName: PT.string,
environment: PT.string,
auth: PT.shape(),
};

function mapStateToProps(state) {
const auth = state.auth && state.auth.profile ? { ...state.auth } : null;
return {
auth,
};
}

export default connect(
mapStateToProps,
)(Article);
12 changes: 10 additions & 2 deletions src/shared/components/Contentful/Article/themes/default.scss
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,8 @@
padding: 8px 9px;
margin-right: 10px;

.circleGreenIcon {
.circleGreenIcon,
.circleGreenIconDisabled {
border-radius: 100%;
width: 42px;
height: 42px;
Expand All @@ -431,7 +432,8 @@
background-color: #12c188;
}

.circleRedIcon {
.circleRedIcon,
.circleRedIconDisabled {
border-radius: 100%;
width: 42px;
height: 42px;
Expand All @@ -444,6 +446,12 @@
transform: rotateX(-180deg);
}

.circleGreenIconDisabled,
.circleRedIconDisabled {
pointer-events: none;
opacity: 0.5;
}

span {
@include barlow-bold;

Expand Down
9 changes: 7 additions & 2 deletions src/shared/components/Gigs/LoginModal/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const progressBarMid = 'https://images.ctfassets.net/b5f1djy59z3a/517ZRt9geweW3Q
const progressBarXS = 'https://images.ctfassets.net/b5f1djy59z3a/6QxH7uVKCngtzBaXDn3Od1/3e0222a1ce773cead3f3a45f291f43a6/progress-bar-mobile.svg';
const blobPurple = 'https://images.ctfassets.net/b5f1djy59z3a/1ZRCwp1uoShcES16lQmeu/ba084734120ffedebcb92b4e3fa2d667/blob-purple.svg';

function LoginModal({ retUrl, onCancel }) {
function LoginModal({ retUrl, onCancel, utmSource }) {
return (
<Modal
theme={modalStyle}
Expand All @@ -56,7 +56,7 @@ function LoginModal({ retUrl, onCancel }) {
<div className={modalStyle.ctaButtons}>
<PrimaryButton
onClick={() => {
window.location = `${config.URL.AUTH}/member/registration?retUrl=${encodeURIComponent(retUrl)}&mode=signUp&utm_source=gig_listing`;
window.location = `${config.URL.AUTH}/member/registration?retUrl=${encodeURIComponent(retUrl)}&mode=signUp&utm_source=${utmSource}`;
}}
theme={{
button: buttonThemes.tc['primary-green-md'],
Expand All @@ -72,9 +72,14 @@ function LoginModal({ retUrl, onCancel }) {
);
}

LoginModal.defaultProps = {
utmSource: 'gig_listing',
};

LoginModal.propTypes = {
retUrl: PT.string.isRequired,
onCancel: PT.func.isRequired,
utmSource: PT.string,
};

export default LoginModal;
70 changes: 70 additions & 0 deletions src/shared/components/LoginModal/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Generic Login Modal Dialog
*/
/* global window */

import PT from 'prop-types';
import React from 'react';
import { Modal, PrimaryButton } from 'topcoder-react-ui-kit';
import { config, Link } from 'topcoder-react-utils';
import tc from 'components/buttons/themed/tc.scss';
import modalStyle from './modal.scss';

/** Themes for buttons
* those overwrite PrimaryButton style to match achieve various styles.
* Should implement pattern of classes.
*/
const buttonThemes = {
tc,
};

function LoginModal({
onCancel,
retUrl,
utmSource,
modalTitle,
modalText,
infoNode,
}) {
return (
<Modal
onCancel={onCancel}
theme={modalStyle}
>
<div className={modalStyle.loginRequired}>
<h3 className={modalStyle.title}>{modalTitle}</h3>
<p className={modalStyle.loginMsg}>{modalText}</p>
<div className={modalStyle.ctaButtons}>
<PrimaryButton
onClick={() => {
window.location = `${config.URL.AUTH}/member?retUrl=${encodeURIComponent(retUrl)}`;
}}
theme={{
button: buttonThemes.tc['primary-green-md'],
}}
>
LOGIN
</PrimaryButton>
<Link to={`${config.URL.AUTH}/member/registration?retUrl=${encodeURIComponent(retUrl)}&mode=signUp${utmSource ? `&utm_source=${utmSource}` : ''}`} className={buttonThemes.tc['primary-white-md']}>REGISTER</Link>
</div>
{infoNode}
</div>
</Modal>
);
}

LoginModal.defaultProps = {
utmSource: null,
infoNode: null,
};

LoginModal.propTypes = {
onCancel: PT.func.isRequired,
retUrl: PT.string.isRequired,
utmSource: PT.string,
modalTitle: PT.string.isRequired,
modalText: PT.string.isRequired,
infoNode: PT.node,
};

export default LoginModal;
Loading