feat(settings): Expand Settings page functionality (#16664)
* fix(layout): Fix Settings layout in firefox * chore(availableForHire): Remove available for hire setting * feat(helpers): Use helper components for Settings layout * fix(map): Fix undefined lang requested * feat(settings): Expand Settings page functionality * chore(pledge): Remove pledge from Settings * fix(about): Adjust AboutSettings layout * fix(portfolio): Improve PortfolioSettings layout * fix(email): Improve EmailSettings layout * fix(settings): Align save buttons with form fields * fix(AHP): Format AHP * fix(DangerZone): Adjust DangerZone layout * fix(projectSettings): Change Button Copy * fix(CertSettings): Fix certificate claim logic * chore(lint): Lint
This commit is contained in:
committed by
Quincy Larson
parent
9f034f4f79
commit
24ef69cf7a
@ -26,9 +26,9 @@
|
|||||||
.content-mixin(baseline) { align-content: baseline; }
|
.content-mixin(baseline) { align-content: baseline; }
|
||||||
.content-mixin(@_) {}
|
.content-mixin(@_) {}
|
||||||
|
|
||||||
.grid(@direction: row; @items: none; @justify: none; @content: none) {
|
.grid(@direction: row; @items: none; @justify: none; @content: none; @wrap: wrap) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: @wrap;
|
||||||
flex-direction: @direction;
|
flex-direction: @direction;
|
||||||
|
|
||||||
.justify-mixin(@justify);
|
.justify-mixin(@justify);
|
||||||
|
@ -37,7 +37,7 @@ export default handleActions(
|
|||||||
[types.makeToast]: (state, { payload: toast }) => [
|
[types.makeToast]: (state, { payload: toast }) => [
|
||||||
...state,
|
...state,
|
||||||
toast
|
toast
|
||||||
],
|
].filter(toast => !!toast.message),
|
||||||
[types.removeToast]: (state, { payload: key }) => state.filter(
|
[types.removeToast]: (state, { payload: key }) => state.filter(
|
||||||
toast => toast.key !== key
|
toast => toast.key !== key
|
||||||
)
|
)
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
// Here we invert the order in which
|
// Here we invert the order in which
|
||||||
// they are painted using css so the
|
// they are painted using css so the
|
||||||
// nav is on top again
|
// nav is on top again
|
||||||
.grid(@direction: column);
|
.grid(@direction: column; @wrap: nowrap);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import { findIndex, invert, pick, property } from 'lodash';
|
||||||
|
import uuid from 'uuid/v4';
|
||||||
import {
|
import {
|
||||||
composeReducers,
|
composeReducers,
|
||||||
createAction,
|
createAction,
|
||||||
@ -8,11 +9,16 @@ import {
|
|||||||
|
|
||||||
import { themes } from '../../utils/themes';
|
import { themes } from '../../utils/themes';
|
||||||
import { types as challenges } from '../routes/Challenges/redux';
|
import { types as challenges } from '../routes/Challenges/redux';
|
||||||
|
import { usernameSelector } from '../redux';
|
||||||
|
|
||||||
export const ns = 'entities';
|
export const ns = 'entities';
|
||||||
export const getNS = state => state[ns];
|
export const getNS = state => state[ns];
|
||||||
export const entitiesSelector = getNS;
|
export const entitiesSelector = getNS;
|
||||||
export const types = createTypes([
|
export const types = createTypes([
|
||||||
|
'addPortfolioItem',
|
||||||
|
'optoUpdatePortfolio',
|
||||||
|
'regresPortfolio',
|
||||||
|
'updateMultipleUserFlags',
|
||||||
'updateTheme',
|
'updateTheme',
|
||||||
'updateUserFlag',
|
'updateUserFlag',
|
||||||
'updateUserEmail',
|
'updateUserEmail',
|
||||||
@ -20,6 +26,18 @@ export const types = createTypes([
|
|||||||
'updateUserCurrentChallenge'
|
'updateUserCurrentChallenge'
|
||||||
], ns);
|
], ns);
|
||||||
|
|
||||||
|
// addPortfolioItem(...PortfolioItem) => Action
|
||||||
|
export const addPortfolioItem = createAction(types.addPortfolioItem);
|
||||||
|
// optoUpdatePortfolio(...PortfolioItem) => Action
|
||||||
|
export const optoUpdatePortfolio = createAction(types.optoUpdatePortfolio);
|
||||||
|
// regresPortfolio(id: String) => Action
|
||||||
|
export const regresPortfolio = createAction(types.regresPortfolio);
|
||||||
|
|
||||||
|
// updateMultipleUserFlags({ username: String, flags: { String }) => Action
|
||||||
|
export const updateMultipleUserFlags = createAction(
|
||||||
|
types.updateMultipleUserFlags
|
||||||
|
);
|
||||||
|
|
||||||
// updateUserFlag(username: String, flag: String) => Action
|
// updateUserFlag(username: String, flag: String) => Action
|
||||||
export const updateUserFlag = createAction(
|
export const updateUserFlag = createAction(
|
||||||
types.updateUserFlag,
|
types.updateUserFlag,
|
||||||
@ -41,7 +59,8 @@ export const updateUserCurrentChallenge = createAction(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// entity meta creators
|
// entity meta creators
|
||||||
const getEntityAction = _.property('meta.entitiesAction');
|
const getEntityAction = property('meta.entitiesAction');
|
||||||
|
|
||||||
export const updateThemeMetacreator = (username, theme) => ({
|
export const updateThemeMetacreator = (username, theme) => ({
|
||||||
entitiesAction: {
|
entitiesAction: {
|
||||||
type: types.updateTheme,
|
type: types.updateTheme,
|
||||||
@ -52,6 +71,16 @@ export const updateThemeMetacreator = (username, theme) => ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function emptyPortfolio() {
|
||||||
|
return {
|
||||||
|
id: uuid(),
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
url: '',
|
||||||
|
image: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
superBlock: {},
|
superBlock: {},
|
||||||
block: {},
|
block: {},
|
||||||
@ -59,13 +88,56 @@ const defaultState = {
|
|||||||
user: {}
|
user: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function selectiveChallengeTitleSelector(state, dashedName) {
|
||||||
|
return getNS(state).challenge[dashedName].title;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function portfolioSelector(state, props) {
|
||||||
|
const username = usernameSelector(state);
|
||||||
|
const { portfolio } = getNS(state).user[username];
|
||||||
|
const pIndex = findIndex(portfolio, p => p.id === props.id);
|
||||||
|
return portfolio[pIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectsSelector(state) {
|
||||||
|
const blocks = getNS(state).block;
|
||||||
|
const challengeNameToIdMap = invert(challengeIdToNameMapSelector(state));
|
||||||
|
return Object.keys(blocks)
|
||||||
|
.filter(key =>
|
||||||
|
key.includes('projects') && !key.includes('coding-interview')
|
||||||
|
)
|
||||||
|
.map(key => blocks[key])
|
||||||
|
.map(({ name, challenges, superBlock }) => {
|
||||||
|
const projectChallengeDashNames = challenges
|
||||||
|
// remove any project intros
|
||||||
|
.filter(chal => !chal.includes('get-set-for'));
|
||||||
|
const projectChallenges = projectChallengeDashNames
|
||||||
|
.map(dashedName => selectiveChallengeTitleSelector(state, dashedName));
|
||||||
|
return {
|
||||||
|
projectBlockName: name,
|
||||||
|
superBlock,
|
||||||
|
challenges: projectChallenges,
|
||||||
|
challengeNameIdMap: pick(
|
||||||
|
challengeNameToIdMap,
|
||||||
|
projectChallengeDashNames
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function challengeIdToNameMapSelector(state) {
|
||||||
|
return getNS(state).challengeIdToName || {};
|
||||||
|
}
|
||||||
|
|
||||||
export const challengeMapSelector = state => getNS(state).challenge || {};
|
export const challengeMapSelector = state => getNS(state).challenge || {};
|
||||||
|
|
||||||
export function makeBlockSelector(block) {
|
export function makeBlockSelector(block) {
|
||||||
return state => {
|
return state => {
|
||||||
const blockMap = getNS(state).block || {};
|
const blockMap = getNS(state).block || {};
|
||||||
return blockMap[block] || {};
|
return blockMap[block] || {};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeSuperBlockSelector(name) {
|
export function makeSuperBlockSelector(name) {
|
||||||
return state => {
|
return state => {
|
||||||
const superBlock = getNS(state).superBlock || {};
|
const superBlock = getNS(state).superBlock || {};
|
||||||
@ -121,6 +193,61 @@ export default composeReducers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
[types.addPortfolioItem]: (state, { payload: username }) => ({
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[username]: {
|
||||||
|
...state.user[username],
|
||||||
|
portfolio: [
|
||||||
|
...state.user[username].portfolio,
|
||||||
|
emptyPortfolio()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[types.optoUpdatePortfolio]: (
|
||||||
|
state,
|
||||||
|
{ payload: { username, portfolio }}
|
||||||
|
) => {
|
||||||
|
const currentPortfolio = state.user[username].portfolio.slice(0);
|
||||||
|
const pIndex = findIndex(currentPortfolio, p => p.id === portfolio.id);
|
||||||
|
const updatedPortfolio = currentPortfolio;
|
||||||
|
updatedPortfolio[pIndex] = portfolio;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[username]: {
|
||||||
|
...state.user[username],
|
||||||
|
portfolio: updatedPortfolio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[types.regresPortfolio]: (state, { payload: { username, id } }) => ({
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[username]: {
|
||||||
|
...state.user[username],
|
||||||
|
portfolio: state.user[username].portfolio.filter(p => p.id !== id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[types.updateMultipleUserFlags]: (
|
||||||
|
state,
|
||||||
|
{ payload: { username, flags }}
|
||||||
|
) => ({
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[username]: {
|
||||||
|
...state.user[username],
|
||||||
|
...flags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
[types.updateUserFlag]: (state, { payload: { username, flag } }) => ({
|
[types.updateUserFlag]: (state, { payload: { username, flag } }) => ({
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
|
11
common/app/helperComponents/ButtonSpacer.jsx
Normal file
11
common/app/helperComponents/ButtonSpacer.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function ButtonSpacer() {
|
||||||
|
return (
|
||||||
|
<div className='button-spacer' />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonSpacer.displayName = 'ButtonSpacer';
|
||||||
|
|
||||||
|
export default ButtonSpacer;
|
20
common/app/helperComponents/FullWidthRow.jsx
Normal file
20
common/app/helperComponents/FullWidthRow.jsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Row, Col } from 'react-bootstrap';
|
||||||
|
|
||||||
|
function FullWidthRow({ children }) {
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col sm={ 8 } smOffset={ 2 } xs={ 12 }>
|
||||||
|
{ children }
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FullWidthRow.displayName = 'FullWidthRow';
|
||||||
|
FullWidthRow.propTypes = {
|
||||||
|
children: PropTypes.any
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FullWidthRow;
|
23
common/app/helperComponents/Loader.jsx
Normal file
23
common/app/helperComponents/Loader.jsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Helmet from 'react-helmet';
|
||||||
|
|
||||||
|
function Loader() {
|
||||||
|
return (
|
||||||
|
<div className='full-size'>
|
||||||
|
<Helmet>
|
||||||
|
<link href='/css/loader.css' rel='stylesheet' />
|
||||||
|
</Helmet>
|
||||||
|
<div className='loader full-size'>
|
||||||
|
<div className='ball-scale-ripple-multiple'>
|
||||||
|
<div/>
|
||||||
|
<div/>
|
||||||
|
<div/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader.displayName = 'Loader';
|
||||||
|
|
||||||
|
export default Loader;
|
11
common/app/helperComponents/Spacer.jsx
Normal file
11
common/app/helperComponents/Spacer.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function Spacer() {
|
||||||
|
return (
|
||||||
|
<div className='spacer' />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer.displayName = 'Spacer';
|
||||||
|
|
||||||
|
export default Spacer;
|
4
common/app/helperComponents/index.js
Normal file
4
common/app/helperComponents/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { default as FullWidthRow } from './FullWidthRow.jsx';
|
||||||
|
export { default as Loader } from './Loader.jsx';
|
||||||
|
export { default as Spacer } from './Spacer.jsx';
|
||||||
|
export { default as ButtonSpacer } from './ButtonSpacer.jsx';
|
@ -54,11 +54,14 @@ export function fetchChallengesEpic(
|
|||||||
{ getState },
|
{ getState },
|
||||||
{ services }
|
{ services }
|
||||||
) {
|
) {
|
||||||
return actions::ofType(types.appMounted)
|
return actions::ofType(
|
||||||
|
types.appMounted,
|
||||||
|
types.updateChallenges
|
||||||
|
)
|
||||||
.flatMapLatest(() => {
|
.flatMapLatest(() => {
|
||||||
const lang = langSelector(getState());
|
const lang = langSelector(getState());
|
||||||
const options = {
|
const options = {
|
||||||
lang,
|
params: { lang },
|
||||||
service: 'map'
|
service: 'map'
|
||||||
};
|
};
|
||||||
return services.readService$(options)
|
return services.readService$(options)
|
||||||
|
@ -40,7 +40,7 @@ export const types = createTypes([
|
|||||||
|
|
||||||
createAsyncTypes('fetchChallenge'),
|
createAsyncTypes('fetchChallenge'),
|
||||||
createAsyncTypes('fetchChallenges'),
|
createAsyncTypes('fetchChallenges'),
|
||||||
|
'updateChallenges',
|
||||||
createAsyncTypes('fetchUser'),
|
createAsyncTypes('fetchUser'),
|
||||||
'showSignIn',
|
'showSignIn',
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ export const fetchChallengesCompleted = createAction(
|
|||||||
(entities, result) => ({ entities, result }),
|
(entities, result) => ({ entities, result }),
|
||||||
entities => ({ entities })
|
entities => ({ entities })
|
||||||
);
|
);
|
||||||
|
export const updateChallenges = createAction(types.updateChallenges);
|
||||||
// updateTitle(title: String) => Action
|
// updateTitle(title: String) => Action
|
||||||
export const updateTitle = createAction(types.updateTitle);
|
export const updateTitle = createAction(types.updateTitle);
|
||||||
|
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Button, Row, Col } from 'react-bootstrap';
|
|
||||||
import TB from './Toggle-Button';
|
|
||||||
import FA from 'react-fontawesome';
|
|
||||||
|
|
||||||
import ns from './ns.json';
|
|
||||||
import { onRouteUpdateEmail } from './redux';
|
|
||||||
import { Link } from '../../Router';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
email: PropTypes.string,
|
|
||||||
sendMonthlyEmail: PropTypes.bool,
|
|
||||||
sendNotificationEmail: PropTypes.bool,
|
|
||||||
sendQuincyEmail: PropTypes.bool,
|
|
||||||
toggleMonthlyEmail: PropTypes.func.isRequired,
|
|
||||||
toggleNotificationEmail: PropTypes.func.isRequired,
|
|
||||||
toggleQuincyEmail: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export function UpdateEmailButton() {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
style={{ textDecoration: 'none' }}
|
|
||||||
to={ onRouteUpdateEmail() }
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-link-social'
|
|
||||||
>
|
|
||||||
<FA name='envelope' />
|
|
||||||
Update my Email
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EmailSettings({
|
|
||||||
email,
|
|
||||||
sendMonthlyEmail,
|
|
||||||
sendNotificationEmail,
|
|
||||||
sendQuincyEmail,
|
|
||||||
toggleMonthlyEmail,
|
|
||||||
toggleNotificationEmail,
|
|
||||||
toggleQuincyEmail
|
|
||||||
}) {
|
|
||||||
if (!email) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<p className='large-p text-center'>
|
|
||||||
You don't have an email id associated to this account.
|
|
||||||
</p>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<UpdateEmailButton />
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={ `${ns}-email-container` }>
|
|
||||||
<Row>
|
|
||||||
<p className='large-p text-center'>
|
|
||||||
<em>{ email }</em>
|
|
||||||
</p>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<UpdateEmailButton />
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Col sm={ 8 }>
|
|
||||||
<p className='large-p'>
|
|
||||||
Send me announcement emails
|
|
||||||
<br />
|
|
||||||
(we'll send you these every Thursday)
|
|
||||||
</p>
|
|
||||||
</Col>
|
|
||||||
<Col sm={ 4 }>
|
|
||||||
<TB
|
|
||||||
name='monthly-email'
|
|
||||||
onChange={ toggleMonthlyEmail }
|
|
||||||
value={ sendMonthlyEmail }
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Col sm={ 8 }>
|
|
||||||
<p className='large-p'>
|
|
||||||
Send me notification emails
|
|
||||||
<br />
|
|
||||||
(these will pertain to your account)
|
|
||||||
</p>
|
|
||||||
</Col>
|
|
||||||
<Col sm={ 4 }>
|
|
||||||
<TB
|
|
||||||
name='notifications-email'
|
|
||||||
onChange={ toggleNotificationEmail }
|
|
||||||
value={ sendNotificationEmail }
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Col sm={ 8 }>
|
|
||||||
<p className='large-p'>
|
|
||||||
Send me Quincy's weekly email
|
|
||||||
<br />
|
|
||||||
(with new articles every Tuesday)
|
|
||||||
</p>
|
|
||||||
</Col>
|
|
||||||
<Col sm={ 4 }>
|
|
||||||
<TB
|
|
||||||
name='quincy-email'
|
|
||||||
onChange={ toggleQuincyEmail }
|
|
||||||
value={ sendQuincyEmail }
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EmailSettings.displayName = 'EmailSettings';
|
|
||||||
EmailSettings.propTypes = propTypes;
|
|
@ -1,39 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Button, Row, Col } from 'react-bootstrap';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
isAvailableForHire: PropTypes.bool,
|
|
||||||
toggle: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function JobSettings({ isAvailableForHire, toggle }) {
|
|
||||||
const className = classnames({
|
|
||||||
active: isAvailableForHire,
|
|
||||||
'btn-toggle': true
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<Row>
|
|
||||||
<Col xs={ 9 }>
|
|
||||||
<p className='large-p'>
|
|
||||||
Available for hire?
|
|
||||||
</p>
|
|
||||||
</Col>
|
|
||||||
<Col xs={ 3 }>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='primary'
|
|
||||||
className={ className }
|
|
||||||
onClick={ toggle }
|
|
||||||
>
|
|
||||||
{ isAvailableForHire ? 'Yes' : 'No' }
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
JobSettings.displayName = 'JobSettings';
|
|
||||||
JobSettings.propTypes = propTypes;
|
|
@ -1,42 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Button, Row, Col } from 'react-bootstrap';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
isLocked: PropTypes.bool,
|
|
||||||
toggle: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LockSettings({ isLocked, toggle }) {
|
|
||||||
const className = classnames({
|
|
||||||
'positive-20': true,
|
|
||||||
active: isLocked,
|
|
||||||
'btn-toggle': true
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<Row>
|
|
||||||
<Col xs={ 9 }>
|
|
||||||
<p className='large-p'>
|
|
||||||
Make all of my solutions private
|
|
||||||
<br />
|
|
||||||
(this disables your certificates)
|
|
||||||
</p>
|
|
||||||
</Col>
|
|
||||||
<Col xs={ 3 }>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='primary'
|
|
||||||
className={ className }
|
|
||||||
onClick={ toggle }
|
|
||||||
>
|
|
||||||
{ isLocked ? 'On' : 'Off' }
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LockSettings.displayName = 'LockSettings';
|
|
||||||
LockSettings.propTypes = propTypes;
|
|
@ -1,99 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Button, Row, Col } from 'react-bootstrap';
|
|
||||||
|
|
||||||
import ns from './ns.json';
|
|
||||||
|
|
||||||
// actual chars required to give buttons some height
|
|
||||||
// whitespace alone is no good
|
|
||||||
const placeholderString = (
|
|
||||||
<span className='placeholder-string'>
|
|
||||||
placeholder text of 28 chars
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
const shortString = (
|
|
||||||
<span className='placeholder-string'>
|
|
||||||
placeholder
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
export default function SettingsSkeleton() {
|
|
||||||
return (
|
|
||||||
<div className={ `${ns}-container ${ns}-skeleton` }>
|
|
||||||
<Row>
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
className='btn-link-social'
|
|
||||||
>
|
|
||||||
{ placeholderString }
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
className='btn-link-social'
|
|
||||||
>
|
|
||||||
{ placeholderString }
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
className='btn-link-social'
|
|
||||||
>
|
|
||||||
{ placeholderString }
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<h1 className='text-center'>{ placeholderString }</h1>
|
|
||||||
<h2 className='text-center'>{ shortString }</h2>
|
|
||||||
<Row>
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
className='btn-link-social'
|
|
||||||
>
|
|
||||||
{ placeholderString }
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
className='btn-link-social'
|
|
||||||
>
|
|
||||||
{ placeholderString }
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
className='btn-link-social'
|
|
||||||
>
|
|
||||||
{ placeholderString }
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
className='btn-link-social'
|
|
||||||
>
|
|
||||||
{ placeholderString }
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div className='spacer' />
|
|
||||||
<h2 className='text-center'>{ placeholderString }</h2>
|
|
||||||
<Row>
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
className='btn-link-social'
|
|
||||||
>
|
|
||||||
{ placeholderString }
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div className='spacer' />
|
|
||||||
<h2 className='text-center'>{ placeholderString }</h2>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -3,58 +3,39 @@ import React from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { Button, Row, Col } from 'react-bootstrap';
|
import { Button } from 'react-bootstrap';
|
||||||
import FA from 'react-fontawesome';
|
import FA from 'react-fontawesome';
|
||||||
|
|
||||||
import ns from './ns.json';
|
import ns from './ns.json';
|
||||||
import LockedSettings from './Locked-Settings.jsx';
|
import { FullWidthRow, Spacer, Loader } from '../../helperComponents';
|
||||||
import JobSettings from './Job-Settings.jsx';
|
import AboutSettings from './components/About-Settings.jsx';
|
||||||
import SocialSettings from './Social-Settings.jsx';
|
import InternetSettings from './components/Internet-Settings.jsx';
|
||||||
import EmailSettings from './Email-Setting.jsx';
|
import EmailSettings from './components/Email-Settings.jsx';
|
||||||
import LanguageSettings from './Language-Settings.jsx';
|
import DangerZone from './components/DangerZone.jsx';
|
||||||
import SettingsSkeleton from './Settings-Skeleton.jsx';
|
import LanguageSettings from './components/Language-Settings.jsx';
|
||||||
|
import CertificationSettings from './components/Cert-Settings.jsx';
|
||||||
|
import PortfolioSettings from './components/Portfolio-Settings.jsx';
|
||||||
|
import Honesty from './components/Honesty.jsx';
|
||||||
|
|
||||||
import { toggleUserFlag } from './redux';
|
|
||||||
import {
|
import {
|
||||||
toggleNightMode,
|
toggleNightMode,
|
||||||
updateTitle,
|
updateTitle,
|
||||||
|
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
userSelector,
|
usernameSelector,
|
||||||
themeSelector,
|
themeSelector,
|
||||||
hardGoTo
|
hardGoTo
|
||||||
} from '../../redux';
|
} from '../../redux';
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
userSelector,
|
usernameSelector,
|
||||||
themeSelector,
|
themeSelector,
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
(
|
(
|
||||||
{
|
username,
|
||||||
username,
|
|
||||||
email,
|
|
||||||
isAvailableForHire,
|
|
||||||
isLocked,
|
|
||||||
isGithubCool,
|
|
||||||
isTwitter,
|
|
||||||
isLinkedIn,
|
|
||||||
sendMonthlyEmail,
|
|
||||||
sendNotificationEmail,
|
|
||||||
sendQuincyEmail
|
|
||||||
},
|
|
||||||
theme,
|
theme,
|
||||||
showLoading,
|
showLoading,
|
||||||
) => ({
|
) => ({
|
||||||
currentTheme: theme,
|
|
||||||
email,
|
|
||||||
isAvailableForHire,
|
|
||||||
isGithubCool,
|
|
||||||
isLinkedIn,
|
|
||||||
isLocked,
|
|
||||||
isTwitter,
|
|
||||||
sendMonthlyEmail,
|
|
||||||
sendNotificationEmail,
|
|
||||||
sendQuincyEmail,
|
|
||||||
showLoading,
|
showLoading,
|
||||||
username
|
username
|
||||||
})
|
})
|
||||||
@ -62,37 +43,13 @@ const mapStateToProps = createSelector(
|
|||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
hardGoTo,
|
hardGoTo,
|
||||||
toggleIsAvailableForHire: () => toggleUserFlag('isAvailableForHire'),
|
|
||||||
toggleIsLocked: () => toggleUserFlag('isLocked'),
|
|
||||||
toggleMonthlyEmail: () => toggleUserFlag('sendMonthlyEmail'),
|
|
||||||
toggleNightMode,
|
toggleNightMode,
|
||||||
toggleNotificationEmail: () => toggleUserFlag('sendNotificationEmail'),
|
|
||||||
toggleQuincyEmail: () => toggleUserFlag('sendQuincyEmail'),
|
|
||||||
updateTitle
|
updateTitle
|
||||||
};
|
};
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
children: PropTypes.element,
|
|
||||||
currentTheme: PropTypes.string,
|
|
||||||
email: PropTypes.string,
|
|
||||||
hardGoTo: PropTypes.func.isRequired,
|
hardGoTo: PropTypes.func.isRequired,
|
||||||
initialLang: PropTypes.string,
|
|
||||||
isAvailableForHire: PropTypes.bool,
|
|
||||||
isGithubCool: PropTypes.bool,
|
|
||||||
isLinkedIn: PropTypes.bool,
|
|
||||||
isLocked: PropTypes.bool,
|
|
||||||
isTwitter: PropTypes.bool,
|
|
||||||
lang: PropTypes.string,
|
|
||||||
sendMonthlyEmail: PropTypes.bool,
|
|
||||||
sendNotificationEmail: PropTypes.bool,
|
|
||||||
sendQuincyEmail: PropTypes.bool,
|
|
||||||
showLoading: PropTypes.bool,
|
showLoading: PropTypes.bool,
|
||||||
toggleIsAvailableForHire: PropTypes.func.isRequired,
|
|
||||||
toggleIsLocked: PropTypes.func.isRequired,
|
|
||||||
toggleMonthlyEmail: PropTypes.func.isRequired,
|
|
||||||
toggleNightMode: PropTypes.func.isRequired,
|
|
||||||
toggleNotificationEmail: PropTypes.func.isRequired,
|
|
||||||
toggleQuincyEmail: PropTypes.func.isRequired,
|
|
||||||
updateMyLang: PropTypes.func,
|
updateMyLang: PropTypes.func,
|
||||||
updateTitle: PropTypes.func.isRequired,
|
updateTitle: PropTypes.func.isRequired,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
@ -121,188 +78,51 @@ export class Settings extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
currentTheme,
|
|
||||||
email,
|
|
||||||
isAvailableForHire,
|
|
||||||
isGithubCool,
|
|
||||||
isLinkedIn,
|
|
||||||
isLocked,
|
|
||||||
isTwitter,
|
|
||||||
sendMonthlyEmail,
|
|
||||||
sendNotificationEmail,
|
|
||||||
sendQuincyEmail,
|
|
||||||
showLoading,
|
showLoading,
|
||||||
toggleIsAvailableForHire,
|
|
||||||
toggleIsLocked,
|
|
||||||
toggleMonthlyEmail,
|
|
||||||
toggleNightMode,
|
|
||||||
toggleNotificationEmail,
|
|
||||||
toggleQuincyEmail,
|
|
||||||
username
|
username
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (!username && showLoading) {
|
if (!username && showLoading) {
|
||||||
return <SettingsSkeleton />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={ `${ns}-container` }>
|
<div className={ `${ns}-container` }>
|
||||||
<Row>
|
<FullWidthRow>
|
||||||
<Col xs={ 12 }>
|
<Button
|
||||||
<Button
|
block={ true }
|
||||||
block={ true }
|
bsSize='lg'
|
||||||
bsSize='lg'
|
bsStyle='primary'
|
||||||
bsStyle='primary'
|
className='btn-link-social'
|
||||||
className='btn-link-social'
|
href={ `/${username}` }
|
||||||
href={ `/${username}` }
|
|
||||||
>
|
|
||||||
<FA name='user' />
|
|
||||||
Show me my public profile
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-link-social'
|
|
||||||
href={ '/signout' }
|
|
||||||
>
|
|
||||||
Sign me out of freeCodeCamp
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<h1 className='text-center'>Settings for your Account</h1>
|
|
||||||
<h2 className='text-center'>Actions</h2>
|
|
||||||
<Row>
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-link-social'
|
|
||||||
onClick={ () => toggleNightMode(username, currentTheme) }
|
|
||||||
>
|
|
||||||
Toggle Night Mode
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
<SocialSettings
|
|
||||||
isGithubCool={ isGithubCool }
|
|
||||||
isLinkedIn={ isLinkedIn }
|
|
||||||
isTwitter={ isTwitter }
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div className='spacer' />
|
|
||||||
<h2 className='text-center'>Account Settings</h2>
|
|
||||||
<Row>
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-link-social'
|
|
||||||
href='/commit'
|
|
||||||
>
|
|
||||||
Edit my pledge
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div className='spacer' />
|
|
||||||
<h2 className='text-center'>Privacy Settings</h2>
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 6 }
|
|
||||||
mdOffset={ 3 }
|
|
||||||
sm={ 8 }
|
|
||||||
smOffset={ 2 }
|
|
||||||
xs={ 12 }
|
|
||||||
>
|
>
|
||||||
<LockedSettings
|
<FA name='user' />
|
||||||
isLocked={ isLocked }
|
Show me my public profile
|
||||||
toggle={ toggleIsLocked }
|
</Button>
|
||||||
/>
|
<Button
|
||||||
</Col>
|
block={ true }
|
||||||
</Row>
|
bsSize='lg'
|
||||||
<div className='spacer' />
|
bsStyle='primary'
|
||||||
<h2 className='text-center'>Job Search Settings</h2>
|
className='btn-link-social'
|
||||||
<Row>
|
href={ '/signout' }
|
||||||
<Col
|
|
||||||
md={ 6 }
|
|
||||||
mdOffset={ 3 }
|
|
||||||
sm={ 8 }
|
|
||||||
smOffset={ 2 }
|
|
||||||
xs={ 12 }
|
|
||||||
>
|
>
|
||||||
<JobSettings
|
Sign me out of freeCodeCamp
|
||||||
isAvailableForHire={ isAvailableForHire }
|
</Button>
|
||||||
toggle={ toggleIsAvailableForHire }
|
</FullWidthRow>
|
||||||
/>
|
<h1 className='text-center'>{ `Account Settings for ${username}` }</h1>
|
||||||
</Col>
|
<AboutSettings />
|
||||||
</Row>
|
<Spacer />
|
||||||
<div className='spacer' />
|
<EmailSettings />
|
||||||
<h2 className='text-center'>Email Settings</h2>
|
<Spacer />
|
||||||
<Row>
|
<LanguageSettings />
|
||||||
<Col
|
<Spacer />
|
||||||
md={ 6 }
|
<InternetSettings />
|
||||||
mdOffset={ 3 }
|
<Spacer />
|
||||||
sm={ 8 }
|
<PortfolioSettings />
|
||||||
smOffset={ 2 }
|
<Spacer />
|
||||||
xs={ 12 }
|
<CertificationSettings />
|
||||||
>
|
<Spacer />
|
||||||
<EmailSettings
|
<Honesty />
|
||||||
email={ email }
|
<Spacer />
|
||||||
sendMonthlyEmail={ sendMonthlyEmail }
|
<DangerZone />
|
||||||
sendNotificationEmail={ sendNotificationEmail }
|
|
||||||
sendQuincyEmail={ sendQuincyEmail }
|
|
||||||
toggleMonthlyEmail={ toggleMonthlyEmail }
|
|
||||||
toggleNotificationEmail={ toggleNotificationEmail }
|
|
||||||
toggleQuincyEmail={ toggleQuincyEmail }
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div className='spacer' />
|
|
||||||
<h2 className='text-center'>Display challenges in:</h2>
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 6 }
|
|
||||||
mdOffset={ 3 }
|
|
||||||
sm={ 8 }
|
|
||||||
smOffset={ 2 }
|
|
||||||
xs={ 12 }
|
|
||||||
>
|
|
||||||
<LanguageSettings />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div className='spacer' />
|
|
||||||
<h2 className='text-center'>Danger Zone</h2>
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 6 }
|
|
||||||
mdOffset={ 3 }
|
|
||||||
sm={ 8 }
|
|
||||||
smOffset={ 2 }
|
|
||||||
xs={ 12 }
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='danger'
|
|
||||||
className='btn-link-social'
|
|
||||||
href='/delete-my-account'
|
|
||||||
>
|
|
||||||
Delete my freeCodeCamp account
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='danger'
|
|
||||||
className='btn-link-social'
|
|
||||||
href='/reset-my-progress'
|
|
||||||
>
|
|
||||||
Reset all of my progress and brownie points
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Button } from 'react-bootstrap';
|
|
||||||
import FA from 'react-fontawesome';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
isGithubCool: PropTypes.bool,
|
|
||||||
isLinkedIn: PropTypes.bool,
|
|
||||||
isTwitter: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SocialSettings({
|
|
||||||
isGithubCool,
|
|
||||||
isTwitter,
|
|
||||||
isLinkedIn
|
|
||||||
}) {
|
|
||||||
const githubCopy = isGithubCool ?
|
|
||||||
'Update my profile from GitHub' :
|
|
||||||
'Link my GitHub to enable my public profile';
|
|
||||||
const buttons = [
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
className='btn-link-social btn-github'
|
|
||||||
href='/link/github'
|
|
||||||
key='github'
|
|
||||||
>
|
|
||||||
<FA name='github' />
|
|
||||||
{ githubCopy }
|
|
||||||
</Button>
|
|
||||||
];
|
|
||||||
const socials = [
|
|
||||||
{
|
|
||||||
isActive: isTwitter,
|
|
||||||
identifier: 'twitter',
|
|
||||||
text: 'Twitter'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isActive: isLinkedIn,
|
|
||||||
identifier: 'linkedin',
|
|
||||||
text: 'LinkedIn'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
if (isGithubCool) {
|
|
||||||
socials.forEach(({ isActive, identifier, text }) => {
|
|
||||||
const socialClass = classnames(
|
|
||||||
'btn-link-social',
|
|
||||||
`btn-${identifier}`,
|
|
||||||
{ active: isActive }
|
|
||||||
);
|
|
||||||
const socialLink = isActive ?
|
|
||||||
`/account/unlink/${identifier}` :
|
|
||||||
`/link/${identifier}`;
|
|
||||||
const socialText = isTwitter ?
|
|
||||||
`Remove my ${text} from my portfolio` :
|
|
||||||
`Add my ${text} to my portfolio`;
|
|
||||||
buttons.push((
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='lg'
|
|
||||||
className={ socialClass }
|
|
||||||
href={ socialLink }
|
|
||||||
key={ identifier }
|
|
||||||
>
|
|
||||||
<FA name={ identifier } />
|
|
||||||
{ socialText }
|
|
||||||
</Button>
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (<div>{ buttons }</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
SocialSettings.displayName = 'SocialSettings';
|
|
||||||
SocialSettings.propTypes = propTypes;
|
|
236
common/app/routes/Settings/components/About-Settings.jsx
Normal file
236
common/app/routes/Settings/components/About-Settings.jsx
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import {
|
||||||
|
Nav,
|
||||||
|
NavItem
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
|
import { FullWidthRow, Spacer } from '../../../helperComponents';
|
||||||
|
import LockedSettings from './Locked-Settings.jsx';
|
||||||
|
import ThemeSettings from './ThemeSettings.jsx';
|
||||||
|
import Camper from './Camper.jsx';
|
||||||
|
import UsernameSettings from './UsernameSettings.jsx';
|
||||||
|
import SectionHeader from './SectionHeader.jsx';
|
||||||
|
import { userSelector, toggleNightMode } from '../../../redux';
|
||||||
|
import {
|
||||||
|
updateUserBackend
|
||||||
|
} from '../redux';
|
||||||
|
import {
|
||||||
|
BlockSaveButton,
|
||||||
|
BlockSaveWrapper,
|
||||||
|
FormFields,
|
||||||
|
maxLength,
|
||||||
|
validURL
|
||||||
|
} from '../formHelpers';
|
||||||
|
|
||||||
|
const max288Char = maxLength(288);
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
(
|
||||||
|
{
|
||||||
|
about,
|
||||||
|
isLocked,
|
||||||
|
location,
|
||||||
|
name,
|
||||||
|
picture,
|
||||||
|
points,
|
||||||
|
theme,
|
||||||
|
username
|
||||||
|
},
|
||||||
|
) => ({
|
||||||
|
about,
|
||||||
|
currentTheme: theme,
|
||||||
|
initialValues: { name, location, about, picture },
|
||||||
|
isLocked,
|
||||||
|
location,
|
||||||
|
name,
|
||||||
|
picture,
|
||||||
|
points,
|
||||||
|
username
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const formFields = [ 'name', 'location', 'picture', 'about' ];
|
||||||
|
|
||||||
|
function validator(values) {
|
||||||
|
const errors = {};
|
||||||
|
const {
|
||||||
|
about,
|
||||||
|
picture
|
||||||
|
} = values;
|
||||||
|
errors.about = max288Char(about);
|
||||||
|
errors.picutre = validURL(picture);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
toggleNightMode,
|
||||||
|
updateUserBackend
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
about: PropTypes.string,
|
||||||
|
currentTheme: PropTypes.string,
|
||||||
|
fields: PropTypes.object,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
isLocked: PropTypes.bool,
|
||||||
|
location: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
picture: PropTypes.string,
|
||||||
|
points: PropTypes.number,
|
||||||
|
toggleNightMode: PropTypes.func.isRequired,
|
||||||
|
updateUserBackend: PropTypes.func.isRequired,
|
||||||
|
username: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
class AboutSettings extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.handleTabSelect = this.handleTabSelect.bind(this);
|
||||||
|
this.renderEdit = this.renderEdit.bind(this);
|
||||||
|
this.renderPreview = this.renderPreview.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
view: 'edit'
|
||||||
|
};
|
||||||
|
this.show = {
|
||||||
|
edit: this.renderEdit,
|
||||||
|
preview: this.renderPreview
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(values) {
|
||||||
|
this.props.updateUserBackend(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabSelect(key) {
|
||||||
|
this.setState(state => ({
|
||||||
|
...state,
|
||||||
|
view: key
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEdit() {
|
||||||
|
const { fields } = this.props;
|
||||||
|
const options = {
|
||||||
|
types: {
|
||||||
|
about: 'textarea',
|
||||||
|
picture: 'url'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FormFields
|
||||||
|
fields={ fields }
|
||||||
|
options={ options }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPreview() {
|
||||||
|
const {
|
||||||
|
fields: {
|
||||||
|
picture: { value: picture },
|
||||||
|
name: { value: name },
|
||||||
|
location: { value: location },
|
||||||
|
about: { value: about }
|
||||||
|
},
|
||||||
|
points,
|
||||||
|
username
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<Camper
|
||||||
|
about={ about }
|
||||||
|
location={ location }
|
||||||
|
name={ name }
|
||||||
|
picture={ picture }
|
||||||
|
points={ points }
|
||||||
|
username={ username }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
currentTheme,
|
||||||
|
fields: { _meta: { allPristine } },
|
||||||
|
handleSubmit,
|
||||||
|
isLocked,
|
||||||
|
toggleNightMode,
|
||||||
|
updateUserBackend,
|
||||||
|
username
|
||||||
|
} = this.props;
|
||||||
|
const { view } = this.state;
|
||||||
|
|
||||||
|
const toggleIsLocked = () => updateUserBackend({ isLocked: !isLocked });
|
||||||
|
const toggleTheme = () => toggleNightMode(username, currentTheme);
|
||||||
|
return (
|
||||||
|
<div className='about-settings'>
|
||||||
|
<SectionHeader>
|
||||||
|
About Settings
|
||||||
|
</SectionHeader>
|
||||||
|
<UsernameSettings username={ username }/>
|
||||||
|
<FullWidthRow>
|
||||||
|
<Nav
|
||||||
|
activeKey={ view }
|
||||||
|
bsStyle='tabs'
|
||||||
|
className='edit-preview-tabs'
|
||||||
|
onSelect={k => this.handleTabSelect(k)}
|
||||||
|
>
|
||||||
|
<NavItem eventKey='edit' title='Edit Bio'>
|
||||||
|
Edit Bio
|
||||||
|
</NavItem>
|
||||||
|
<NavItem eventKey='preview' title='Preview Bio'>
|
||||||
|
Preview Bio
|
||||||
|
</NavItem>
|
||||||
|
</Nav>
|
||||||
|
</FullWidthRow>
|
||||||
|
<br />
|
||||||
|
<FullWidthRow>
|
||||||
|
<form id='camper-identity' onSubmit={ handleSubmit(this.handleSubmit) }>
|
||||||
|
{
|
||||||
|
this.show[view]()
|
||||||
|
}
|
||||||
|
<BlockSaveWrapper>
|
||||||
|
<BlockSaveButton disabled={ allPristine } />
|
||||||
|
</BlockSaveWrapper>
|
||||||
|
</form>
|
||||||
|
</FullWidthRow>
|
||||||
|
<Spacer />
|
||||||
|
<FullWidthRow>
|
||||||
|
<LockedSettings
|
||||||
|
isLocked={ isLocked }
|
||||||
|
toggleIsLocked={ toggleIsLocked }
|
||||||
|
/>
|
||||||
|
</FullWidthRow>
|
||||||
|
<FullWidthRow>
|
||||||
|
<ThemeSettings
|
||||||
|
currentTheme={ currentTheme }
|
||||||
|
toggleNightMode={ toggleTheme }
|
||||||
|
/>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AboutSettings.displayName = 'AboutSettings';
|
||||||
|
AboutSettings.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default reduxForm(
|
||||||
|
{
|
||||||
|
form: 'account-settings',
|
||||||
|
fields: formFields,
|
||||||
|
validate: validator
|
||||||
|
},
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(AboutSettings);
|
58
common/app/routes/Settings/components/Camper.jsx
Normal file
58
common/app/routes/Settings/components/Camper.jsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import SocialIcons from './SocialIcons.jsx';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
about: PropTypes.string,
|
||||||
|
location: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
picture: PropTypes.string,
|
||||||
|
points: PropTypes.number,
|
||||||
|
username: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
function pluralise(word, condition) {
|
||||||
|
return condition ? word + 's' : word;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Camper({
|
||||||
|
name,
|
||||||
|
username,
|
||||||
|
location,
|
||||||
|
points,
|
||||||
|
picture,
|
||||||
|
about
|
||||||
|
}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<Col className='avatar-container' xs={ 12 }>
|
||||||
|
<img
|
||||||
|
alt={ username + '\'s profile picture' }
|
||||||
|
className='avatar'
|
||||||
|
src={ picture }
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<br />
|
||||||
|
<SocialIcons />
|
||||||
|
<br/>
|
||||||
|
<h2 className='text-center username'>@{ username }</h2>
|
||||||
|
{ name && <p className='text-center name'>{ name }</p> }
|
||||||
|
{ location && <p className='text-center location'>{ location }</p> }
|
||||||
|
{ about && <p className='bio text-center'>{ about }</p> }
|
||||||
|
<p className='text-center points'>
|
||||||
|
{ `${points} ${pluralise('point', points > 1)}` }
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Camper.displayName = 'Camper';
|
||||||
|
Camper.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default Camper;
|
247
common/app/routes/Settings/components/Cert-Settings.jsx
Normal file
247
common/app/routes/Settings/components/Cert-Settings.jsx
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import { FullWidthRow } from '../../../helperComponents';
|
||||||
|
import { Form } from '../formHelpers';
|
||||||
|
import JSAlgoAndDSForm from './JSAlgoAndDSForm.jsx';
|
||||||
|
import SectionHeader from './SectionHeader.jsx';
|
||||||
|
import { projectsSelector } from '../../../entities';
|
||||||
|
import { claimCert, updateUserBackend } from '../redux';
|
||||||
|
import { fetchChallenges, userSelector, hardGoTo } from '../../../redux';
|
||||||
|
import {
|
||||||
|
buildUserProjectsMap,
|
||||||
|
jsProjectSuperBlock
|
||||||
|
} from '../utils/buildUserProjectsMap';
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
projectsSelector,
|
||||||
|
(
|
||||||
|
{
|
||||||
|
challengeMap,
|
||||||
|
isRespWebDesignCert,
|
||||||
|
is2018DataVisCert,
|
||||||
|
isFrontEndLibsCert,
|
||||||
|
isJsAlgoDataStructCert,
|
||||||
|
isApisMicroservicesCert,
|
||||||
|
isInfosecQaCert,
|
||||||
|
username
|
||||||
|
},
|
||||||
|
projects
|
||||||
|
) => ({
|
||||||
|
projects,
|
||||||
|
userProjects: projects
|
||||||
|
.map(block => buildUserProjectsMap(block, challengeMap))
|
||||||
|
.reduce((projects, current) => ({
|
||||||
|
...projects,
|
||||||
|
...current
|
||||||
|
}), {}),
|
||||||
|
blockNameIsCertMap: {
|
||||||
|
'Applied Responsive Web Design Projects': isRespWebDesignCert,
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
'JavaScript Algorithms and Data Structures Projects': isJsAlgoDataStructCert,
|
||||||
|
/* eslint-enable max-len */
|
||||||
|
'Front End Libraries Projects': isFrontEndLibsCert,
|
||||||
|
'Data Visualization Projects': is2018DataVisCert,
|
||||||
|
'API and Microservice Projects': isApisMicroservicesCert,
|
||||||
|
'Information Security and Quality Assurance Projects': isInfosecQaCert
|
||||||
|
},
|
||||||
|
username
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
claimCert,
|
||||||
|
fetchChallenges,
|
||||||
|
hardGoTo,
|
||||||
|
updateUserBackend
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool),
|
||||||
|
claimCert: PropTypes.func.isRequired,
|
||||||
|
fetchChallenges: PropTypes.func.isRequired,
|
||||||
|
hardGoTo: PropTypes.func.isRequired,
|
||||||
|
projects: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
projectBlockName: PropTypes.string,
|
||||||
|
challenges: PropTypes.arrayOf(PropTypes.string)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
superBlock: PropTypes.string,
|
||||||
|
updateUserBackend: PropTypes.func.isRequired,
|
||||||
|
userProjects: PropTypes.objectOf(
|
||||||
|
PropTypes.objectOf(PropTypes.oneOfType(
|
||||||
|
[
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.object
|
||||||
|
]
|
||||||
|
))
|
||||||
|
),
|
||||||
|
username: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
class CertificationSettings extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { projects } = this.props;
|
||||||
|
if (!projects.length) {
|
||||||
|
this.props.fetchChallenges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(values) {
|
||||||
|
const { id } = values;
|
||||||
|
const fullForm = _.values(values)
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(_.isString)
|
||||||
|
// 5 projects + 1 id prop
|
||||||
|
.length === 6;
|
||||||
|
const valuesSaved = _.values(this.props.userProjects[id])
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(_.isString)
|
||||||
|
.length === 6;
|
||||||
|
if (fullForm && valuesSaved) {
|
||||||
|
return this.props.claimCert(id);
|
||||||
|
}
|
||||||
|
const { projects } = this.props;
|
||||||
|
const pIndex = _.findIndex(projects, p => p.superBlock === id);
|
||||||
|
values.nameToIdMap = projects[pIndex].challengeNameIdMap;
|
||||||
|
return this.props.updateUserBackend({
|
||||||
|
projects: {
|
||||||
|
[id]: values
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
blockNameIsCertMap,
|
||||||
|
claimCert,
|
||||||
|
hardGoTo,
|
||||||
|
projects,
|
||||||
|
userProjects,
|
||||||
|
username
|
||||||
|
} = this.props;
|
||||||
|
if (!projects.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SectionHeader>
|
||||||
|
Certification Settings
|
||||||
|
</SectionHeader>
|
||||||
|
<FullWidthRow>
|
||||||
|
<p>
|
||||||
|
Add links to the live demos of your projects as you finish them.
|
||||||
|
Then, once you have added all 5 projects required for a certificate,
|
||||||
|
you can claim it.
|
||||||
|
</p>
|
||||||
|
</FullWidthRow>
|
||||||
|
{
|
||||||
|
projects.map(({
|
||||||
|
projectBlockName,
|
||||||
|
challenges,
|
||||||
|
superBlock
|
||||||
|
}) => {
|
||||||
|
const isCertClaimed = blockNameIsCertMap[projectBlockName];
|
||||||
|
if (superBlock === jsProjectSuperBlock) {
|
||||||
|
return (
|
||||||
|
<JSAlgoAndDSForm
|
||||||
|
challenges={ challenges }
|
||||||
|
claimCert={ claimCert }
|
||||||
|
hardGoTo={ hardGoTo }
|
||||||
|
isCertClaimed={ isCertClaimed }
|
||||||
|
jsProjects={ userProjects[superBlock] }
|
||||||
|
key={ superBlock }
|
||||||
|
projectBlockName={ projectBlockName }
|
||||||
|
superBlock={ superBlock }
|
||||||
|
username={ username }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const options = challenges
|
||||||
|
.reduce((options, current) => {
|
||||||
|
options.types[current] = 'url';
|
||||||
|
return options;
|
||||||
|
}, { types: {} });
|
||||||
|
|
||||||
|
options.types.id = 'hidden';
|
||||||
|
options.placeholder = false;
|
||||||
|
|
||||||
|
const userValues = userProjects[superBlock] || {};
|
||||||
|
|
||||||
|
if (!userValues.id) {
|
||||||
|
userValues.id = superBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues = challenges
|
||||||
|
.reduce((accu, current) => ({
|
||||||
|
...accu,
|
||||||
|
[current]: ''
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
const completedProjects = _.values(userValues)
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(_.isString)
|
||||||
|
// minus 1 to account for the id
|
||||||
|
.length - 1;
|
||||||
|
|
||||||
|
const fullForm = completedProjects === challenges.length;
|
||||||
|
return (
|
||||||
|
<FullWidthRow key={superBlock}>
|
||||||
|
<h3 className='project-heading'>{ projectBlockName }</h3>
|
||||||
|
<Form
|
||||||
|
buttonText={ fullForm ? 'Claim Certificate' : 'Save Progress' }
|
||||||
|
enableSubmit={ fullForm }
|
||||||
|
formFields={ challenges.concat([ 'id' ]) }
|
||||||
|
hideButton={isCertClaimed}
|
||||||
|
id={ superBlock }
|
||||||
|
initialValues={{
|
||||||
|
...initialValues,
|
||||||
|
...userValues
|
||||||
|
}}
|
||||||
|
options={ options }
|
||||||
|
submit={ this.handleSubmit }
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
isCertClaimed ?
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
href={ `/c/${username}/${superBlock}`}
|
||||||
|
>
|
||||||
|
Show Certificate
|
||||||
|
</Button> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
<hr />
|
||||||
|
</FullWidthRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CertificationSettings.displayName = 'CertificationSettings';
|
||||||
|
CertificationSettings.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(CertificationSettings);
|
106
common/app/routes/Settings/components/DangerZone.jsx
Normal file
106
common/app/routes/Settings/components/DangerZone.jsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Panel, Alert, Button } from 'react-bootstrap';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { ButtonSpacer, FullWidthRow } from '../../../helperComponents';
|
||||||
|
import ResetModal from './ResetModal.jsx';
|
||||||
|
import DeleteModal from './DeleteModal.jsx';
|
||||||
|
import { resetProgress, deleteAccount } from '../redux';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
deleteAccount: PropTypes.func.isRequired,
|
||||||
|
resetProgress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({});
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
deleteAccount,
|
||||||
|
resetProgress
|
||||||
|
};
|
||||||
|
|
||||||
|
class DangerZone extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
delete: false,
|
||||||
|
reset: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.toggleDeleteModal = this.toggleDeleteModal.bind(this);
|
||||||
|
this.toggleResetModal = this.toggleResetModal.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDeleteModal() {
|
||||||
|
return this.setState(state => ({
|
||||||
|
...state,
|
||||||
|
delete: !state.delete
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleResetModal() {
|
||||||
|
return this.setState(state => ({
|
||||||
|
...state,
|
||||||
|
reset: !state.reset
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { resetProgress, deleteAccount } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FullWidthRow>
|
||||||
|
<Panel
|
||||||
|
bsStyle='danger'
|
||||||
|
className='danger-zone-panel'
|
||||||
|
header={<h2><strong>Danger Zone</strong></h2>}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
bsStyle='danger'
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Tread carefully, changes made in this area are permanent.
|
||||||
|
They cannot be undone.
|
||||||
|
</p>
|
||||||
|
</Alert>
|
||||||
|
<FullWidthRow>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='danger'
|
||||||
|
onClick={ this.toggleResetModal }
|
||||||
|
>
|
||||||
|
Reset all of my progress
|
||||||
|
</Button>
|
||||||
|
<ButtonSpacer />
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='danger'
|
||||||
|
onClick={ this.toggleDeleteModal }
|
||||||
|
>
|
||||||
|
Delete my account
|
||||||
|
</Button>
|
||||||
|
</FullWidthRow>
|
||||||
|
</Panel>
|
||||||
|
<ResetModal
|
||||||
|
onHide={ this.toggleResetModal }
|
||||||
|
reset={ resetProgress }
|
||||||
|
show={ this.state.reset }
|
||||||
|
/>
|
||||||
|
<DeleteModal
|
||||||
|
delete={ deleteAccount }
|
||||||
|
onHide={ this.toggleDeleteModal }
|
||||||
|
show={ this.state.delete }
|
||||||
|
/>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DangerZone.displayName = 'DangerZone';
|
||||||
|
DangerZone.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(DangerZone);
|
71
common/app/routes/Settings/components/DeleteModal.jsx
Normal file
71
common/app/routes/Settings/components/DeleteModal.jsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Modal, Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
delete: PropTypes.func.isRequired,
|
||||||
|
onHide: PropTypes.func.isRequired,
|
||||||
|
show: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
function DeleteModal(props) {
|
||||||
|
const { show, onHide } = props;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
aria-labelledby='modal-title'
|
||||||
|
autoFocus={ true }
|
||||||
|
backdrop={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
keyboard={ true }
|
||||||
|
onHide={ onHide }
|
||||||
|
show={ show }
|
||||||
|
>
|
||||||
|
<Modal.Header closeButton={ true }>
|
||||||
|
<Modal.Title id='modal-title'>Delete My Account</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<p>
|
||||||
|
This will really delete all your data, including all your progress
|
||||||
|
and account information.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We won't be able to recover any of it for you later,
|
||||||
|
even if you change your mind.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If there's something we could do better, send us an email instead and
|
||||||
|
we'll do our best:  
|
||||||
|
<a href='mailto:team@freecodecamp.org' title='team@freecodecamp.org'>
|
||||||
|
team@freecodecamp.org
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='success'
|
||||||
|
onClick={ props.onHide }
|
||||||
|
>
|
||||||
|
Nevermind, I don't want to delete my account
|
||||||
|
</Button>
|
||||||
|
<div className='button-spacer' />
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='danger'
|
||||||
|
onClick={ props.delete }
|
||||||
|
>
|
||||||
|
I am 100% certain. Delete everything related to this account
|
||||||
|
</Button>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button onClick={ props.onHide }>Close</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteModal.displayName = 'DeleteModal';
|
||||||
|
DeleteModal.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default DeleteModal;
|
168
common/app/routes/Settings/components/Email-Settings.jsx
Normal file
168
common/app/routes/Settings/components/Email-Settings.jsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
ControlLabel,
|
||||||
|
HelpBlock,
|
||||||
|
Row
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
|
import TB from '../Toggle-Button';
|
||||||
|
import EmailForm from './EmailForm.jsx';
|
||||||
|
import { Link } from '../../../Router';
|
||||||
|
import { FullWidthRow, Spacer } from '../../../helperComponents';
|
||||||
|
import SectionHeader from './SectionHeader.jsx';
|
||||||
|
import { userSelector } from '../../../redux';
|
||||||
|
import { onRouteUpdateEmail, updateMyEmail, updateUserBackend } from '../redux';
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
({
|
||||||
|
email,
|
||||||
|
isEmailVerified,
|
||||||
|
sendQuincyEmail
|
||||||
|
}) => ({
|
||||||
|
email,
|
||||||
|
initialValues: { email },
|
||||||
|
isEmailVerified,
|
||||||
|
sendQuincyEmail
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
updateMyEmail,
|
||||||
|
updateUserBackend
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
email: PropTypes.string,
|
||||||
|
isEmailVerified: PropTypes.bool,
|
||||||
|
options: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
flag: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
bool: PropTypes.bool
|
||||||
|
})
|
||||||
|
),
|
||||||
|
sendQuincyEmail: PropTypes.bool,
|
||||||
|
updateMyEmail: PropTypes.func.isRequired,
|
||||||
|
updateUserBackend: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UpdateEmailButton() {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
style={{ textDecoration: 'none' }}
|
||||||
|
to={ onRouteUpdateEmail() }
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-link-social'
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailSettings extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit({ email }) {
|
||||||
|
|
||||||
|
this.props.updateMyEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
isEmailVerified,
|
||||||
|
sendQuincyEmail
|
||||||
|
} = this.props;
|
||||||
|
if (!email) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FullWidthRow>
|
||||||
|
<p className='large-p text-center'>
|
||||||
|
You do not have an email associated with this account.
|
||||||
|
</p>
|
||||||
|
</FullWidthRow>
|
||||||
|
<FullWidthRow>
|
||||||
|
<UpdateEmailButton />
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className='email-settings'>
|
||||||
|
<SectionHeader>
|
||||||
|
Email Settings
|
||||||
|
</SectionHeader>
|
||||||
|
{
|
||||||
|
isEmailVerified ? null :
|
||||||
|
<FullWidthRow>
|
||||||
|
<HelpBlock>
|
||||||
|
<Alert bsStyle='info'>
|
||||||
|
A change of email adress has not been verified.
|
||||||
|
To use your new email, you must verify it first using the link
|
||||||
|
we sent you.
|
||||||
|
</Alert>
|
||||||
|
</HelpBlock>
|
||||||
|
</FullWidthRow>
|
||||||
|
}
|
||||||
|
<FullWidthRow>
|
||||||
|
<EmailForm
|
||||||
|
initialValues={{ email, confrimEmail: ''}}
|
||||||
|
/>
|
||||||
|
</FullWidthRow>
|
||||||
|
<Spacer />
|
||||||
|
<FullWidthRow>
|
||||||
|
<Row className='inline-form-field' key='sendQuincyEmail'>
|
||||||
|
<Col sm={ 8 }>
|
||||||
|
<ControlLabel htmlFor='sendQuincyEmail'>
|
||||||
|
Send me Quincy's weekly email
|
||||||
|
</ControlLabel>
|
||||||
|
</Col>
|
||||||
|
<Col sm={ 4 }>
|
||||||
|
<TB
|
||||||
|
id='sendQuincyEmail'
|
||||||
|
name='sendQuincyEmail'
|
||||||
|
onChange={
|
||||||
|
() => updateUserBackend({
|
||||||
|
sendQuincyEmail: !sendQuincyEmail
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={ sendQuincyEmail }
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailSettings.displayName = 'EmailSettings';
|
||||||
|
EmailSettings.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default reduxForm(
|
||||||
|
{
|
||||||
|
form: 'email-settings',
|
||||||
|
fields: [ 'email' ]
|
||||||
|
},
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(EmailSettings);
|
160
common/app/routes/Settings/components/EmailForm.jsx
Normal file
160
common/app/routes/Settings/components/EmailForm.jsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import React, { PureComponent} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
ControlLabel,
|
||||||
|
FormControl,
|
||||||
|
HelpBlock,
|
||||||
|
Alert
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
import { updateUserBackend } from '../redux';
|
||||||
|
import { FullWidthRow, Spacer } from '../../../helperComponents';
|
||||||
|
import { BlockSaveButton, BlockSaveWrapper, validEmail } from '../formHelpers';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
email: PropTypes.string,
|
||||||
|
errors: PropTypes.object,
|
||||||
|
fields: PropTypes.objectOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
value: PropTypes.string.isRequired
|
||||||
|
})
|
||||||
|
),
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
updateUserBackend: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchtoProps = { updateUserBackend };
|
||||||
|
|
||||||
|
function validator(values) {
|
||||||
|
const errors = {};
|
||||||
|
const { email = '', confirmEmail = '' } = values;
|
||||||
|
|
||||||
|
errors.email = validEmail(email);
|
||||||
|
if (errors.email || errors.confirmEmail) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
errors.confirmEmail = email.toLowerCase() === confirmEmail.toLowerCase() ?
|
||||||
|
null :
|
||||||
|
'Emails should be the same';
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailForm extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.options = {
|
||||||
|
required: [ 'confirmEmail', 'email' ],
|
||||||
|
types: { confirmemail: 'email', email: 'email' }
|
||||||
|
};
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(values) {
|
||||||
|
const { updateUserBackend } = this.props;
|
||||||
|
const update = {
|
||||||
|
email: values.email
|
||||||
|
};
|
||||||
|
updateUserBackend(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
const {
|
||||||
|
fields: { email, confirmEmail },
|
||||||
|
handleSubmit
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const disableForm = (email.pristine && confirmEmail.pristine) ||
|
||||||
|
(!!email.error || !!confirmEmail.error);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form id='email-form' onSubmit={ handleSubmit(this.handleSubmit) }>
|
||||||
|
<Row className='inline-form-field'>
|
||||||
|
<Col sm={ 3 } xs={ 12 }>
|
||||||
|
<ControlLabel htmlFor='email'>
|
||||||
|
Email
|
||||||
|
</ControlLabel>
|
||||||
|
</Col>
|
||||||
|
<Col sm={ 9 } xs={ 12 }>
|
||||||
|
<FormControl
|
||||||
|
bsSize='lg'
|
||||||
|
id='email'
|
||||||
|
name='email'
|
||||||
|
onChange={ email.onChange }
|
||||||
|
required={ true }
|
||||||
|
type='email'
|
||||||
|
value={ email.value }
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<FullWidthRow>
|
||||||
|
{
|
||||||
|
!email.pristine && email.error ?
|
||||||
|
<HelpBlock>
|
||||||
|
<Alert bsStyle='danger'>
|
||||||
|
{ email.error }
|
||||||
|
</Alert>
|
||||||
|
</HelpBlock> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</FullWidthRow>
|
||||||
|
<Row className='inline-form-field'>
|
||||||
|
<Col sm={ 3 } xs={ 12 }>
|
||||||
|
<ControlLabel htmlFor='confirm-email'>
|
||||||
|
Confirm Email
|
||||||
|
</ControlLabel>
|
||||||
|
</Col>
|
||||||
|
<Col sm={ 9 } xs={ 12 }>
|
||||||
|
<FormControl
|
||||||
|
bsSize='lg'
|
||||||
|
id='confirm-email'
|
||||||
|
name='confirm-email'
|
||||||
|
onChange={ confirmEmail.onChange }
|
||||||
|
required={ true }
|
||||||
|
type='email'
|
||||||
|
value={ confirmEmail.value }
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<FullWidthRow>
|
||||||
|
{
|
||||||
|
!confirmEmail.pristine && confirmEmail.error ?
|
||||||
|
<HelpBlock>
|
||||||
|
<Alert bsStyle='danger'>
|
||||||
|
{ confirmEmail.error }
|
||||||
|
</Alert>
|
||||||
|
</HelpBlock> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</FullWidthRow>
|
||||||
|
<Spacer />
|
||||||
|
<BlockSaveWrapper>
|
||||||
|
<BlockSaveButton disabled={ disableForm } />
|
||||||
|
</BlockSaveWrapper>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailForm.displayName = 'EmailForm';
|
||||||
|
EmailForm.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default reduxForm(
|
||||||
|
{
|
||||||
|
form: 'email-form',
|
||||||
|
fields: [ 'confirmEmail', 'email' ],
|
||||||
|
validate: validator
|
||||||
|
},
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchtoProps
|
||||||
|
)(EmailForm);
|
95
common/app/routes/Settings/components/Honesty.jsx
Normal file
95
common/app/routes/Settings/components/Honesty.jsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button, Panel } from 'react-bootstrap';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { FullWidthRow } from '../../../helperComponents';
|
||||||
|
import SectionHeader from './SectionHeader.jsx';
|
||||||
|
import { userSelector } from '../../../redux';
|
||||||
|
import academicPolicy from '../../../../resource/academicPolicy';
|
||||||
|
import { updateUserBackend } from '../redux';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
isHonest: PropTypes.bool,
|
||||||
|
policy: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
updateUserBackend: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
({ isHonest }) => ({
|
||||||
|
policy: academicPolicy,
|
||||||
|
isHonest
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapDispatchToProps = { updateUserBackend };
|
||||||
|
|
||||||
|
class Honesty extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
showHonesty: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleAgreeClick = this.handleAgreeClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAgreeClick() {
|
||||||
|
this.props.updateUserBackend({ isHonest: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { policy, isHonest } = this.props;
|
||||||
|
const isHonestAgreed = (
|
||||||
|
<Panel bsStyle='info'>
|
||||||
|
<p>
|
||||||
|
You have already accepted our Academic Honesty Policy
|
||||||
|
</p>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
const agreeButton = (
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsStyle='primary'
|
||||||
|
onClick={ this.handleAgreeClick }
|
||||||
|
>
|
||||||
|
Agree
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className='honesty-policy'>
|
||||||
|
<SectionHeader>
|
||||||
|
Academic Honesty Policy
|
||||||
|
</SectionHeader>
|
||||||
|
<FullWidthRow>
|
||||||
|
<Panel>
|
||||||
|
{
|
||||||
|
policy.map(
|
||||||
|
(line, i) => (
|
||||||
|
<p
|
||||||
|
dangerouslySetInnerHTML={{ __html: line }}
|
||||||
|
key={ '' + i + line.slice(0, 10) }
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<br />
|
||||||
|
{
|
||||||
|
isHonest ?
|
||||||
|
isHonestAgreed :
|
||||||
|
agreeButton
|
||||||
|
}
|
||||||
|
</Panel>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Honesty.displayName = 'Honesty';
|
||||||
|
Honesty.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Honesty);
|
109
common/app/routes/Settings/components/Internet-Settings.jsx
Normal file
109
common/app/routes/Settings/components/Internet-Settings.jsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
|
||||||
|
import { FullWidthRow, Spacer } from '../../../helperComponents';
|
||||||
|
import { BlockSaveButton, BlockSaveWrapper, FormFields } from '../formHelpers';
|
||||||
|
import SectionHeader from './SectionHeader.jsx';
|
||||||
|
import { userSelector } from '../../../redux';
|
||||||
|
import { updateUserBackend } from '../redux';
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
({
|
||||||
|
githubURL = '',
|
||||||
|
linkedin = '',
|
||||||
|
twitter = '',
|
||||||
|
website = ''
|
||||||
|
}) => ({
|
||||||
|
initialValues: {
|
||||||
|
githubURL,
|
||||||
|
linkedin,
|
||||||
|
twitter,
|
||||||
|
website
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const formFields = [ 'githubURL', 'linkedin', 'twitter', 'website' ];
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
updateUserBackend
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
fields: PropTypes.object,
|
||||||
|
githubURL: PropTypes.string,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
linkedin: PropTypes.string,
|
||||||
|
twitter: PropTypes.string,
|
||||||
|
updateUserBackend: PropTypes.func.isRequired,
|
||||||
|
username: PropTypes.string,
|
||||||
|
website: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
class InternetSettings extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(values) {
|
||||||
|
console.log(values);
|
||||||
|
this.props.updateUserBackend(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
fields,
|
||||||
|
fields: { _meta: { allPristine } },
|
||||||
|
handleSubmit
|
||||||
|
} = this.props;
|
||||||
|
const options = {
|
||||||
|
types: formFields.reduce(
|
||||||
|
(all, current) => ({ ...all, [current]: 'url' }),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
placeholder: false
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className='internet-settings'>
|
||||||
|
<SectionHeader>
|
||||||
|
Your Internet Presence
|
||||||
|
</SectionHeader>
|
||||||
|
<FullWidthRow>
|
||||||
|
<form
|
||||||
|
id='internet-handle-settings'
|
||||||
|
onSubmit={ handleSubmit(this.handleSubmit) }
|
||||||
|
>
|
||||||
|
<FormFields
|
||||||
|
fields={ fields }
|
||||||
|
options={ options }
|
||||||
|
/>
|
||||||
|
<Spacer />
|
||||||
|
<BlockSaveWrapper>
|
||||||
|
<BlockSaveButton disabled={ allPristine }/>
|
||||||
|
</BlockSaveWrapper>
|
||||||
|
</form>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InternetSettings.displayName = 'InternetSettings';
|
||||||
|
InternetSettings.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default reduxForm(
|
||||||
|
{
|
||||||
|
form: 'internet-settings',
|
||||||
|
fields: formFields
|
||||||
|
},
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(InternetSettings);
|
120
common/app/routes/Settings/components/JSAlgoAndDSForm.jsx
Normal file
120
common/app/routes/Settings/components/JSAlgoAndDSForm.jsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { kebabCase } from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import { FullWidthRow } from '../../../helperComponents';
|
||||||
|
import { BlockSaveButton } from '../formHelpers';
|
||||||
|
import { Link } from '../../../Router';
|
||||||
|
import SolutionViewer from './SolutionViewer.jsx';
|
||||||
|
|
||||||
|
const jsFormPropTypes = {
|
||||||
|
challenges: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
claimCert: PropTypes.func.isRequired,
|
||||||
|
hardGoTo: PropTypes.func.isRequired,
|
||||||
|
isCertClaimed: PropTypes.bool,
|
||||||
|
jsProjects: PropTypes.objectOf(PropTypes.object),
|
||||||
|
projectBlockName: PropTypes.string,
|
||||||
|
superBlock: PropTypes.string,
|
||||||
|
username: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsProjectPath = '/challenges/javascript-algorithms-and-data-structures-' +
|
||||||
|
'projects/';
|
||||||
|
|
||||||
|
class JSAlgoAndDSForm extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {};
|
||||||
|
this.handleSolutionToggle = this.handleSolutionToggle.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSolutionToggle(e) {
|
||||||
|
e.persist();
|
||||||
|
return this.setState(state => ({
|
||||||
|
...state,
|
||||||
|
[e.target.id]: !state[e.target.id]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const { username, superBlock, isCertClaimed } = this.props;
|
||||||
|
if (isCertClaimed) {
|
||||||
|
return this.props.hardGoTo(`/c/${username}/${superBlock}`);
|
||||||
|
}
|
||||||
|
return this.props.claimCert(superBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
projectBlockName,
|
||||||
|
challenges = [],
|
||||||
|
jsProjects = {},
|
||||||
|
isCertClaimed
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<FullWidthRow>
|
||||||
|
<h3 className='project-heading'>{ projectBlockName }</h3>
|
||||||
|
<p>
|
||||||
|
To complete this certification, you must first complete the
|
||||||
|
JavaScript Algorithms and Data Structures project challenges
|
||||||
|
</p>
|
||||||
|
<ul className='solution-list'>
|
||||||
|
{
|
||||||
|
challenges.map(challenge => (
|
||||||
|
<div key={ challenge }>
|
||||||
|
<li className='solution-list-item'>
|
||||||
|
<p>{ challenge }</p>
|
||||||
|
{
|
||||||
|
Object.keys(jsProjects[challenge]).length ?
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
id={ challenge }
|
||||||
|
onClick={ this.handleSolutionToggle }
|
||||||
|
>
|
||||||
|
{ this.state[challenge] ? 'Hide' : 'Show' } Solution
|
||||||
|
</Button>
|
||||||
|
</div> :
|
||||||
|
<Link to={`${jsProjectPath}${kebabCase(challenge)}`}>
|
||||||
|
<Button
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
>
|
||||||
|
Complete Project
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
{
|
||||||
|
this.state[challenge] ?
|
||||||
|
<SolutionViewer files={ jsProjects[challenge] } /> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
{
|
||||||
|
Object.keys(jsProjects).length === 6 ?
|
||||||
|
<form onSubmit={ this.handleSubmit }>
|
||||||
|
<BlockSaveButton>
|
||||||
|
{ isCertClaimed ? 'Show' : 'Claim'} Certificate
|
||||||
|
</BlockSaveButton>
|
||||||
|
</form> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
<hr />
|
||||||
|
</FullWidthRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JSAlgoAndDSForm.displayName = 'JSAlgoAndDSForm';
|
||||||
|
JSAlgoAndDSForm.propTypes = jsFormPropTypes;
|
||||||
|
|
||||||
|
export default JSAlgoAndDSForm;
|
@ -2,11 +2,19 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import { FormControl, FormGroup } from 'react-bootstrap';
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
ControlLabel,
|
||||||
|
Row,
|
||||||
|
Col
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
import { updateMyLang } from './redux';
|
import { updateMyLang } from '../redux';
|
||||||
import { userSelector } from '../../redux';
|
import { userSelector } from '../../../redux';
|
||||||
import langs from '../../../utils/supported-languages';
|
import langs from '../../../../utils/supported-languages';
|
||||||
|
import { FullWidthRow } from '../../../helperComponents';
|
||||||
|
import SectionHeader from './SectionHeader.jsx';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
fields: PropTypes.object,
|
fields: PropTypes.object,
|
||||||
@ -85,18 +93,35 @@ export class LanguageSettings extends React.Component {
|
|||||||
fields: { lang: { name, value } }
|
fields: { lang: { name, value } }
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<div>
|
||||||
<FormControl
|
<SectionHeader>
|
||||||
className='btn btn-block btn-primary btn-link-social btn-lg'
|
Language Settings
|
||||||
componentClass='select'
|
</SectionHeader>
|
||||||
name={ name }
|
<FullWidthRow>
|
||||||
onChange={ this.handleChange }
|
<FormGroup>
|
||||||
style={{ height: '45px' }}
|
<Row>
|
||||||
value={ value }
|
<Col sm={ 4 } xs={ 12 }>
|
||||||
>
|
<ControlLabel htmlFor={ name }>
|
||||||
{ options }
|
Prefered Language for Challenges
|
||||||
</FormControl>
|
</ControlLabel>
|
||||||
</FormGroup>
|
</Col>
|
||||||
|
<Col sm={ 8 } xs={ 12 }>
|
||||||
|
<FormControl
|
||||||
|
className='btn btn-block btn-primary btn-lg'
|
||||||
|
componentClass='select'
|
||||||
|
id={ name }
|
||||||
|
name={ name }
|
||||||
|
onChange={ this.handleChange }
|
||||||
|
style={{ height: '45px' }}
|
||||||
|
value={ value }
|
||||||
|
>
|
||||||
|
{ options }
|
||||||
|
</FormControl>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</FormGroup>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
42
common/app/routes/Settings/components/Locked-Settings.jsx
Normal file
42
common/app/routes/Settings/components/Locked-Settings.jsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
ControlLabel
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
|
import TB from '../Toggle-Button';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
isLocked: PropTypes.bool,
|
||||||
|
toggleIsLocked: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LockSettings({ isLocked, toggleIsLocked }) {
|
||||||
|
return (
|
||||||
|
<Row className='inline-form'>
|
||||||
|
<Col sm={ 8 } xs={ 12 }>
|
||||||
|
<ControlLabel htmlFor='isLocked'>
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
Make all of my solutions private
|
||||||
|
<br />
|
||||||
|
<em>(this disables your certificates)</em>
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
</ControlLabel>
|
||||||
|
</Col>
|
||||||
|
<Col sm={ 4 } xs={ 12 }>
|
||||||
|
<TB
|
||||||
|
name='isLocked'
|
||||||
|
onChange={ toggleIsLocked }
|
||||||
|
value={ isLocked }
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LockSettings.displayName = 'LockSettings';
|
||||||
|
LockSettings.propTypes = propTypes;
|
178
common/app/routes/Settings/components/Portfolio-Settings.jsx
Normal file
178
common/app/routes/Settings/components/Portfolio-Settings.jsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import { FullWidthRow, ButtonSpacer } from '../../../helperComponents';
|
||||||
|
import SectionHeader from './SectionHeader.jsx';
|
||||||
|
import { userSelector } from '../../../redux';
|
||||||
|
import { addPortfolioItem } from '../../../entities';
|
||||||
|
import { updateMyPortfolio, deletePortfolio } from '../redux';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
maxLength,
|
||||||
|
minLength,
|
||||||
|
validURL
|
||||||
|
} from '../formHelpers';
|
||||||
|
|
||||||
|
const minTwoChar = minLength(2);
|
||||||
|
const max288Char = maxLength(288);
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
addPortfolioItem: PropTypes.func.isRequired,
|
||||||
|
deletePortfolio: PropTypes.func.isRequired,
|
||||||
|
picture: PropTypes.string,
|
||||||
|
portfolio: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
description: PropTypes.string,
|
||||||
|
image: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
|
url: PropTypes.string
|
||||||
|
})
|
||||||
|
),
|
||||||
|
updateMyPortfolio: PropTypes.func.isRequired,
|
||||||
|
username: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
({ portfolio, username }) => ({
|
||||||
|
portfolio,
|
||||||
|
username
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
addPortfolioItem,
|
||||||
|
deletePortfolio,
|
||||||
|
updateMyPortfolio
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formFields = [ 'title', 'url', 'image', 'description', 'id' ];
|
||||||
|
const options = {
|
||||||
|
types: {
|
||||||
|
id: 'hidden',
|
||||||
|
url: 'url',
|
||||||
|
image: 'url',
|
||||||
|
description: 'textarea'
|
||||||
|
},
|
||||||
|
required: [ 'url', 'title', 'id' ]
|
||||||
|
};
|
||||||
|
|
||||||
|
function validator(values) {
|
||||||
|
const errors = {};
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
url = '',
|
||||||
|
description = '',
|
||||||
|
image = ''
|
||||||
|
} = values;
|
||||||
|
errors.title = minTwoChar(title);
|
||||||
|
errors.description = max288Char(description);
|
||||||
|
errors.url = url && validURL(url);
|
||||||
|
errors.image = image && validURL(image);
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PortfolioSettings extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleAdd = this.handleAdd.bind(this);
|
||||||
|
this.handleDelete = this.handleDelete.bind(this);
|
||||||
|
this.handleSave = this.handleSave.bind(this);
|
||||||
|
this.renderPortfolio = this.renderPortfolio.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAdd() {
|
||||||
|
this.props.addPortfolioItem(this.props.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDelete(id) {
|
||||||
|
const { deletePortfolio } = this.props;
|
||||||
|
deletePortfolio({ portfolio: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSave(portfolio) {
|
||||||
|
const { updateMyPortfolio } = this.props;
|
||||||
|
updateMyPortfolio(portfolio);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPortfolio(portfolio, index, arr) {
|
||||||
|
const {
|
||||||
|
id
|
||||||
|
} = portfolio;
|
||||||
|
return (
|
||||||
|
<div key={ id }>
|
||||||
|
<FullWidthRow>
|
||||||
|
<Form
|
||||||
|
buttonText='Save portfolio item'
|
||||||
|
formFields={ formFields }
|
||||||
|
id={ id }
|
||||||
|
initialValues={ portfolio }
|
||||||
|
options={ options }
|
||||||
|
submit={ this.handleSave }
|
||||||
|
validate={ validator }
|
||||||
|
/>
|
||||||
|
<ButtonSpacer />
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='danger'
|
||||||
|
id={`delete-${id}`}
|
||||||
|
onClick={ () => this.handleDelete(id) }
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
Remove this portfolio item
|
||||||
|
</Button>
|
||||||
|
{
|
||||||
|
index + 1 !== arr.length && <hr />
|
||||||
|
}
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { portfolio = [] } = this.props;
|
||||||
|
return (
|
||||||
|
<section id='portfolio-settings' >
|
||||||
|
<SectionHeader>
|
||||||
|
Portfolio Settings
|
||||||
|
</SectionHeader>
|
||||||
|
<FullWidthRow>
|
||||||
|
<div className='portfolio-settings-intro'>
|
||||||
|
<p className='p-intro'>
|
||||||
|
Share your non-FreeCodeCamp projects, articles or accepted
|
||||||
|
pull requests.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</FullWidthRow>
|
||||||
|
{
|
||||||
|
portfolio.length ? portfolio.map(this.renderPortfolio) : null
|
||||||
|
}
|
||||||
|
<FullWidthRow>
|
||||||
|
<ButtonSpacer />
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
onClick={ this.handleAdd }
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
Add a new portfolio Item
|
||||||
|
</Button>
|
||||||
|
</FullWidthRow>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PortfolioSettings.displayName = 'PortfolioSettings';
|
||||||
|
PortfolioSettings.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PortfolioSettings);
|
65
common/app/routes/Settings/components/ResetModal.jsx
Normal file
65
common/app/routes/Settings/components/ResetModal.jsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Modal, Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
onHide: PropTypes.func.isRequired,
|
||||||
|
reset: PropTypes.func.isRequired,
|
||||||
|
show: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
function ResetModal(props) {
|
||||||
|
const { show, onHide } = props;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
aria-labelledby='modal-title'
|
||||||
|
autoFocus={ true }
|
||||||
|
backdrop={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
keyboard={ true }
|
||||||
|
onHide={ onHide }
|
||||||
|
show={ show }
|
||||||
|
>
|
||||||
|
<Modal.Header closeButton={ true }>
|
||||||
|
<Modal.Title id='modal-title'>Reset My Progress</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<p>
|
||||||
|
This will really delete all of your progress, points, completed
|
||||||
|
challenges, our records of your projects, any certificates you have,
|
||||||
|
everything.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We won't be able to recover any of it for you later, even if you
|
||||||
|
change your mind.
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='success'
|
||||||
|
onClick={ props.onHide }
|
||||||
|
>
|
||||||
|
Nevermind, I don't want to delete all of my progress
|
||||||
|
</Button>
|
||||||
|
<div className='button-spacer' />
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='danger'
|
||||||
|
onClick={ () =>{ props.reset(); return props.onHide(); } }
|
||||||
|
>
|
||||||
|
Reset everything. I want to start from the beginning
|
||||||
|
</Button>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button onClick={ props.onHide }>Close</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetModal.displayName = 'ResetModal';
|
||||||
|
ResetModal.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ResetModal;
|
25
common/app/routes/Settings/components/SectionHeader.jsx
Normal file
25
common/app/routes/Settings/components/SectionHeader.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { FullWidthRow } from '../../../helperComponents';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.element,
|
||||||
|
PropTypes.node
|
||||||
|
])
|
||||||
|
};
|
||||||
|
|
||||||
|
function SectionHeader({ children }) {
|
||||||
|
return (
|
||||||
|
<FullWidthRow>
|
||||||
|
<h2>{ children }</h2>
|
||||||
|
<hr />
|
||||||
|
</FullWidthRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader.displayName = 'SectionHeader';
|
||||||
|
SectionHeader.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default SectionHeader;
|
134
common/app/routes/Settings/components/SocialIcons.jsx
Normal file
134
common/app/routes/Settings/components/SocialIcons.jsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Col
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
import FontAwesome from 'react-fontawesome';
|
||||||
|
|
||||||
|
import { userSelector } from '../../../redux';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
email: PropTypes.string,
|
||||||
|
githubURL: PropTypes.string,
|
||||||
|
isGithub: PropTypes.bool,
|
||||||
|
isLinkedIn: PropTypes.bool,
|
||||||
|
isTwitter: PropTypes.bool,
|
||||||
|
isWebsite: PropTypes.bool,
|
||||||
|
linkedIn: PropTypes.string,
|
||||||
|
twitter: PropTypes.string,
|
||||||
|
website: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
({
|
||||||
|
githubURL,
|
||||||
|
isLinkedIn,
|
||||||
|
isGithub,
|
||||||
|
isTwitter,
|
||||||
|
isWebsite,
|
||||||
|
linkedIn,
|
||||||
|
twitter,
|
||||||
|
website
|
||||||
|
}) => ({
|
||||||
|
githubURL,
|
||||||
|
isLinkedIn,
|
||||||
|
isGithub,
|
||||||
|
isTwitter,
|
||||||
|
isWebsite,
|
||||||
|
linkedIn,
|
||||||
|
twitter,
|
||||||
|
website
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
function mapDispatchToProps() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkedInIcon(linkedIn) {
|
||||||
|
return (
|
||||||
|
<a href={ linkedIn } rel='no-follow' target='_blank'>
|
||||||
|
<FontAwesome
|
||||||
|
name='linkedin'
|
||||||
|
size='2x'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function githubIcon(ghURL) {
|
||||||
|
return (
|
||||||
|
<a href={ ghURL } rel='no-follow' target='_blank'>
|
||||||
|
<FontAwesome
|
||||||
|
name='github'
|
||||||
|
size='2x'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WebsiteIcon(website) {
|
||||||
|
return (
|
||||||
|
<a href={ website } rel='no-follow' target='_blank'>
|
||||||
|
<FontAwesome
|
||||||
|
name='link'
|
||||||
|
size='2x'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TwitterIcon(handle) {
|
||||||
|
return (
|
||||||
|
<a href={ handle } rel='no-follow' target='_blank' >
|
||||||
|
<FontAwesome
|
||||||
|
name='twitter'
|
||||||
|
size='2x'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SocialIcons(props) {
|
||||||
|
const {
|
||||||
|
githubURL,
|
||||||
|
isLinkedIn,
|
||||||
|
isGithub,
|
||||||
|
isTwitter,
|
||||||
|
isWebsite,
|
||||||
|
linkedIn,
|
||||||
|
twitter,
|
||||||
|
website
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
className='text-center social-media-icons'
|
||||||
|
sm={ 6 }
|
||||||
|
smOffset={ 3 }
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isLinkedIn ? LinkedInIcon(linkedIn) : null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isGithub ? githubIcon(githubURL) : null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isWebsite ? WebsiteIcon(website) : null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isTwitter ? TwitterIcon(twitter) : null
|
||||||
|
}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SocialIcons.displayName = 'SocialIcons';
|
||||||
|
SocialIcons.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(SocialIcons);
|
53
common/app/routes/Settings/components/SolutionViewer.jsx
Normal file
53
common/app/routes/Settings/components/SolutionViewer.jsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Panel } from 'react-bootstrap';
|
||||||
|
import Prism from 'prismjs';
|
||||||
|
import Helmet from 'react-helmet';
|
||||||
|
|
||||||
|
const prismLang = {
|
||||||
|
css: 'css',
|
||||||
|
js: 'javascript',
|
||||||
|
jsx: 'javascript',
|
||||||
|
html: 'markup'
|
||||||
|
};
|
||||||
|
|
||||||
|
function SolutionViewer({ files }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Helmet>
|
||||||
|
<link href='/css/prism.css' rel='stylesheet' />
|
||||||
|
</Helmet>
|
||||||
|
{
|
||||||
|
Object.keys(files)
|
||||||
|
.map(key => files[key])
|
||||||
|
.map(file => (
|
||||||
|
<Panel
|
||||||
|
bsStyle='primary'
|
||||||
|
className='solution-viewer'
|
||||||
|
header={ file.ext.toUpperCase() }
|
||||||
|
key={ file.ext }
|
||||||
|
>
|
||||||
|
<pre>
|
||||||
|
<code
|
||||||
|
className={ `language-${prismLang[file.ext]}` }
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: Prism.highlight(
|
||||||
|
file.contents.trim(),
|
||||||
|
Prism.languages[prismLang[file.ext]]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
</Panel>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SolutionViewer.displayName = 'SolutionViewer';
|
||||||
|
SolutionViewer.propTypes = {
|
||||||
|
files: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SolutionViewer;
|
38
common/app/routes/Settings/components/ThemeSettings.jsx
Normal file
38
common/app/routes/Settings/components/ThemeSettings.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
ControlLabel
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
|
import TB from '../Toggle-Button';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
currentTheme: PropTypes.string.isRequired,
|
||||||
|
toggleNightMode: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ThemeSettings({ currentTheme, toggleNightMode }) {
|
||||||
|
return (
|
||||||
|
<Row className='inline-form'>
|
||||||
|
<Col sm={ 8 } xs={ 12 }>
|
||||||
|
<ControlLabel htmlFor='night-mode'>
|
||||||
|
<p className='settings-title'>
|
||||||
|
<strong>Night Mode</strong>
|
||||||
|
</p>
|
||||||
|
</ControlLabel>
|
||||||
|
</Col>
|
||||||
|
<Col sm={ 4 } xs={ 12 }>
|
||||||
|
<TB
|
||||||
|
name='night-mode'
|
||||||
|
onChange={ () => toggleNightMode(currentTheme) }
|
||||||
|
value={ currentTheme === 'night' }
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeSettings.displayName = 'ThemeSettings';
|
||||||
|
ThemeSettings.propTypes = propTypes;
|
187
common/app/routes/Settings/components/UsernameSettings.jsx
Normal file
187
common/app/routes/Settings/components/UsernameSettings.jsx
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
Col,
|
||||||
|
ControlLabel,
|
||||||
|
FormControl,
|
||||||
|
Alert
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import {
|
||||||
|
settingsSelector,
|
||||||
|
updateUserBackend,
|
||||||
|
validateUsername
|
||||||
|
} from '../redux';
|
||||||
|
import { userSelector } from '../../../redux';
|
||||||
|
import { BlockSaveButton, minLength } from '../formHelpers';
|
||||||
|
import { FullWidthRow } from '../../../helperComponents';
|
||||||
|
|
||||||
|
const minTwoChar = minLength(2);
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
fields: PropTypes.objectOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
error: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
pristine: PropTypes.bool.isRequired,
|
||||||
|
value: PropTypes.string.isRequired
|
||||||
|
})
|
||||||
|
),
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
isValidUsername: PropTypes.bool,
|
||||||
|
submitAction: PropTypes.func.isRequired,
|
||||||
|
username: PropTypes.string,
|
||||||
|
validateUsername: PropTypes.func.isRequired,
|
||||||
|
validating: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
settingsSelector,
|
||||||
|
({ username }, { isValidUsername, validating }) => ({
|
||||||
|
initialValues: { username },
|
||||||
|
isValidUsername,
|
||||||
|
validate: validator,
|
||||||
|
validating
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
validateUsername,
|
||||||
|
submitAction: updateUserBackend
|
||||||
|
};
|
||||||
|
function normalise(str = '') {
|
||||||
|
return str.toLowerCase().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeHandleChange(changeFn, validationAction, valid) {
|
||||||
|
return function handleChange(e) {
|
||||||
|
const { value } = e.target;
|
||||||
|
e.target.value = normalise(value);
|
||||||
|
if (e.target.value && valid) {
|
||||||
|
validationAction(value);
|
||||||
|
}
|
||||||
|
return changeFn(e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validator(values) {
|
||||||
|
const errors = {};
|
||||||
|
const { username } = values;
|
||||||
|
const minWarn = minTwoChar(username);
|
||||||
|
if (minWarn) {
|
||||||
|
errors.username = minWarn;
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
if (username.length === 0) {
|
||||||
|
errors.username = 'Username cannot be empty';
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAlerts(validating, error, isValidUsername) {
|
||||||
|
if (!validating && error) {
|
||||||
|
return (
|
||||||
|
<FullWidthRow>
|
||||||
|
<Alert bsStyle='danger'>
|
||||||
|
{ error }
|
||||||
|
</Alert>
|
||||||
|
</FullWidthRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!validating && !isValidUsername) {
|
||||||
|
return (
|
||||||
|
<FullWidthRow>
|
||||||
|
<Alert bsStyle='danger'>
|
||||||
|
Username not available
|
||||||
|
</Alert>
|
||||||
|
</FullWidthRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (validating) {
|
||||||
|
return (
|
||||||
|
<FullWidthRow>
|
||||||
|
<Alert bsStyle='info'>
|
||||||
|
Validating username
|
||||||
|
</Alert>
|
||||||
|
</FullWidthRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!validating && isValidUsername) {
|
||||||
|
return (
|
||||||
|
<FullWidthRow>
|
||||||
|
<Alert bsStyle='success'>
|
||||||
|
Username is available
|
||||||
|
</Alert>
|
||||||
|
</FullWidthRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UsernameSettings(props) {
|
||||||
|
const {
|
||||||
|
fields: {
|
||||||
|
username: {
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
error,
|
||||||
|
pristine,
|
||||||
|
valid
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSubmit,
|
||||||
|
isValidUsername,
|
||||||
|
submitAction,
|
||||||
|
validateUsername,
|
||||||
|
validating
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
!pristine && renderAlerts(validating, error, isValidUsername)
|
||||||
|
}
|
||||||
|
<FullWidthRow>
|
||||||
|
<form
|
||||||
|
className='inline-form-field'
|
||||||
|
id='usernameSettings'
|
||||||
|
onSubmit={ handleSubmit(submitAction) }
|
||||||
|
>
|
||||||
|
<Col className='inline-form' sm={ 3 } xs={ 12 }>
|
||||||
|
<ControlLabel htmlFor='username-settings'>
|
||||||
|
<strong>Username</strong>
|
||||||
|
</ControlLabel>
|
||||||
|
</Col>
|
||||||
|
<Col sm={ 7 } xs={ 12 }>
|
||||||
|
<FormControl
|
||||||
|
name='username-settings'
|
||||||
|
onChange={ makeHandleChange(onChange, validateUsername, valid) }
|
||||||
|
value={ value }
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col sm={ 2 } xs={ 12 }>
|
||||||
|
<BlockSaveButton disabled={
|
||||||
|
!(isValidUsername && valid && !pristine)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</form>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UsernameSettings.displayName = 'UsernameSettings';
|
||||||
|
UsernameSettings.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default reduxForm(
|
||||||
|
{
|
||||||
|
form: 'usernameSettings',
|
||||||
|
fields: [ 'username' ]
|
||||||
|
},
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(UsernameSettings);
|
24
common/app/routes/Settings/formHelpers/BlockSaveButton.jsx
Normal file
24
common/app/routes/Settings/formHelpers/BlockSaveButton.jsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
function BlockSaveButton(props) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
{...props}
|
||||||
|
type='submit'
|
||||||
|
>
|
||||||
|
{ props.children || 'Save' }
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockSaveButton.displayName = 'BlockSaveButton';
|
||||||
|
BlockSaveButton.propTypes = {
|
||||||
|
children: PropTypes.any
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockSaveButton;
|
23
common/app/routes/Settings/formHelpers/BlockSaveWrapper.jsx
Normal file
23
common/app/routes/Settings/formHelpers/BlockSaveWrapper.jsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
padding: '0 15px'
|
||||||
|
};
|
||||||
|
|
||||||
|
function BlockSaveWrapper({ children }) {
|
||||||
|
return (
|
||||||
|
<div style={ style }>
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockSaveWrapper.displayName = 'BlockSaveWrapper';
|
||||||
|
BlockSaveWrapper.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default BlockSaveWrapper;
|
88
common/app/routes/Settings/formHelpers/Form.jsx
Normal file
88
common/app/routes/Settings/formHelpers/Form.jsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
|
||||||
|
import { FormFields, BlockSaveButton, BlockSaveWrapper } from './';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
buttonText: PropTypes.string,
|
||||||
|
enableSubmit: PropTypes.bool,
|
||||||
|
errors: PropTypes.object,
|
||||||
|
fields: PropTypes.objectOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
value: PropTypes.string.isRequired
|
||||||
|
})
|
||||||
|
),
|
||||||
|
formFields: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
handleSubmit: PropTypes.func,
|
||||||
|
hideButton: PropTypes.bool,
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
initialValues: PropTypes.object,
|
||||||
|
options: PropTypes.shape({
|
||||||
|
ignored: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
required: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
types: PropTypes.objectOf(PropTypes.string)
|
||||||
|
}),
|
||||||
|
submit: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function DynamicForm({
|
||||||
|
// redux-form
|
||||||
|
errors,
|
||||||
|
fields,
|
||||||
|
handleSubmit,
|
||||||
|
fields: { _meta: { allPristine }},
|
||||||
|
|
||||||
|
// HOC
|
||||||
|
buttonText,
|
||||||
|
enableSubmit,
|
||||||
|
hideButton,
|
||||||
|
id,
|
||||||
|
options,
|
||||||
|
submit
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<form id={`dynamic-${id}`} onSubmit={ handleSubmit(submit) }>
|
||||||
|
<FormFields
|
||||||
|
errors={ errors }
|
||||||
|
fields={ fields }
|
||||||
|
options={ options }
|
||||||
|
/>
|
||||||
|
<BlockSaveWrapper>
|
||||||
|
{
|
||||||
|
hideButton ?
|
||||||
|
null :
|
||||||
|
<BlockSaveButton
|
||||||
|
disabled={
|
||||||
|
allPristine && !enableSubmit ||
|
||||||
|
(!!Object.keys(errors).filter(key => errors[key]).length)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
buttonText ? buttonText : null
|
||||||
|
}
|
||||||
|
</BlockSaveButton>
|
||||||
|
}
|
||||||
|
</BlockSaveWrapper>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicForm.displayName = 'DynamicForm';
|
||||||
|
DynamicForm.propTypes = propTypes;
|
||||||
|
|
||||||
|
const DynamicFormWithRedux = reduxForm()(DynamicForm);
|
||||||
|
|
||||||
|
export default function Form(props) {
|
||||||
|
return (
|
||||||
|
<DynamicFormWithRedux
|
||||||
|
{...props}
|
||||||
|
fields={ props.formFields }
|
||||||
|
form={ props.id }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Form.propTypes = propTypes;
|
97
common/app/routes/Settings/formHelpers/FormFields.jsx
Normal file
97
common/app/routes/Settings/formHelpers/FormFields.jsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Col,
|
||||||
|
ControlLabel,
|
||||||
|
FormControl,
|
||||||
|
HelpBlock,
|
||||||
|
Row
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
errors: PropTypes.objectOf(PropTypes.string),
|
||||||
|
fields: PropTypes.objectOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
value: PropTypes.string.isRequired
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
options: PropTypes.shape({
|
||||||
|
errors: PropTypes.objectOf(
|
||||||
|
PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.instanceOf(null)
|
||||||
|
])
|
||||||
|
),
|
||||||
|
ignored: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
placeholder: PropTypes.bool,
|
||||||
|
required: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
types: PropTypes.objectOf(PropTypes.string)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
function FormFields(props) {
|
||||||
|
const { errors = {}, fields, options = {} } = props;
|
||||||
|
const {
|
||||||
|
ignored = [],
|
||||||
|
placeholder = true,
|
||||||
|
required = [],
|
||||||
|
types = {}
|
||||||
|
} = options;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
Object.keys(fields)
|
||||||
|
.filter(field => !ignored.includes(field))
|
||||||
|
.map(key => fields[key])
|
||||||
|
.map(({ name, onChange, value, pristine }) => {
|
||||||
|
const key = _.kebabCase(name);
|
||||||
|
const type = name in types ? types[name] : 'text';
|
||||||
|
return (
|
||||||
|
<Row className='inline-form-field' key={ key }>
|
||||||
|
<Col sm={ 3 } xs={ 12 }>
|
||||||
|
{ type === 'hidden' ?
|
||||||
|
null :
|
||||||
|
<ControlLabel htmlFor={ key }>
|
||||||
|
{ _.startCase(name) }
|
||||||
|
</ControlLabel>
|
||||||
|
}
|
||||||
|
</Col>
|
||||||
|
<Col sm={ 9 } xs={ 12 }>
|
||||||
|
<FormControl
|
||||||
|
bsSize='lg'
|
||||||
|
componentClass={ type === 'textarea' ? type : 'input' }
|
||||||
|
id={ key }
|
||||||
|
name={ name }
|
||||||
|
onChange={ onChange }
|
||||||
|
placeholder={ placeholder ? name : '' }
|
||||||
|
required={ !!required[name] }
|
||||||
|
rows={ 4 }
|
||||||
|
type={ type }
|
||||||
|
value={ value }
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
name in errors && !pristine ?
|
||||||
|
<HelpBlock>
|
||||||
|
<Alert bsStyle='danger'>
|
||||||
|
{ errors[name] }
|
||||||
|
</Alert>
|
||||||
|
</HelpBlock> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FormFields.displayName = 'FormFields';
|
||||||
|
FormFields.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default FormFields;
|
35
common/app/routes/Settings/formHelpers/index.js
Normal file
35
common/app/routes/Settings/formHelpers/index.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { isEmail, isURL } from 'validator';
|
||||||
|
|
||||||
|
/** Components **/
|
||||||
|
|
||||||
|
export { default as BlockSaveButton } from './BlockSaveButton.jsx';
|
||||||
|
export { default as BlockSaveWrapper } from './BlockSaveWrapper.jsx';
|
||||||
|
export { default as Form } from './Form.jsx';
|
||||||
|
export { default as FormFields } from './FormFields.jsx';
|
||||||
|
|
||||||
|
/** Normalise **/
|
||||||
|
|
||||||
|
export function lowerAndTrim(str = '') {
|
||||||
|
return str.toLowerCase().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validation **/
|
||||||
|
|
||||||
|
export function maxLength(max) {
|
||||||
|
return value => value && value.length > max ?
|
||||||
|
`Must be ${max} characters or less` :
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function minLength(min) {
|
||||||
|
return value => value && value.length < min ?
|
||||||
|
`Must be ${min} characters or more` :
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
export function validEmail(email) {
|
||||||
|
return isEmail(email) ? null : 'Must be a valid email';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validURL(str) {
|
||||||
|
return isURL(str) ? null : 'Must be a valid URL';
|
||||||
|
}
|
42
common/app/routes/Settings/redux/certificate-epic.js
Normal file
42
common/app/routes/Settings/redux/certificate-epic.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
import { ofType } from 'redux-epic';
|
||||||
|
|
||||||
|
import { doActionOnError, fetchUser } from '../../../redux';
|
||||||
|
import { makeToast } from '../../../Toasts/redux';
|
||||||
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
|
import {
|
||||||
|
types,
|
||||||
|
claimCertComplete,
|
||||||
|
claimCertError
|
||||||
|
} from '../redux';
|
||||||
|
|
||||||
|
function certificateEpic(actions$, { getState }) {
|
||||||
|
const start = actions$::ofType(types.claimCert.start)
|
||||||
|
.flatMap(({ payload: superBlock }) => {
|
||||||
|
const {
|
||||||
|
app: { csrfToken: _csrf }
|
||||||
|
} = getState();
|
||||||
|
return postJSON$('/certificate/verify', { _csrf, superBlock });
|
||||||
|
})
|
||||||
|
.map(claimCertComplete)
|
||||||
|
.catch(doActionOnError(error => claimCertError(error)));
|
||||||
|
|
||||||
|
const complete = actions$::ofType(types.claimCert.complete)
|
||||||
|
.flatMap(({ meta: { message, success }}) => Observable.if(
|
||||||
|
() => success,
|
||||||
|
Observable.of(fetchUser(), makeToast({ message })),
|
||||||
|
Observable.of(makeToast({ message }))
|
||||||
|
));
|
||||||
|
|
||||||
|
const error = actions$::ofType(types.claimCert.error)
|
||||||
|
.flatMap(error => {
|
||||||
|
return Observable.of(
|
||||||
|
makeToast({ message: 'Something went wrong updating your account' }),
|
||||||
|
{ type: 'error', error}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Observable.merge(start, complete, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default certificateEpic;
|
53
common/app/routes/Settings/redux/danger-zone-epic.js
Normal file
53
common/app/routes/Settings/redux/danger-zone-epic.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
import { ofType } from 'redux-epic';
|
||||||
|
|
||||||
|
import {
|
||||||
|
types,
|
||||||
|
resetProgressError,
|
||||||
|
deleteAccountError,
|
||||||
|
deleteAccountComplete
|
||||||
|
} from './';
|
||||||
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
|
import {
|
||||||
|
doActionOnError,
|
||||||
|
hardGoTo,
|
||||||
|
createErrorObservable
|
||||||
|
} from '../../../redux';
|
||||||
|
|
||||||
|
function dangerZoneEpic(actions$, { getState }) {
|
||||||
|
/** Reset Progress **/
|
||||||
|
const resetStart = actions$::ofType(types.resetProgress.start)
|
||||||
|
.flatMap(() => {
|
||||||
|
const { csrfToken: _csrf } = getState().app;
|
||||||
|
return postJSON$('/account/reset-progress', { _csrf })
|
||||||
|
.map(() => hardGoTo('/'))
|
||||||
|
.catch(doActionOnError(error => resetProgressError(error)));
|
||||||
|
});
|
||||||
|
const resetError = actions$::ofType(types.resetProgress.error)
|
||||||
|
.flatMap(createErrorObservable);
|
||||||
|
|
||||||
|
/** Delete Account **/
|
||||||
|
const deleteStart = actions$::ofType(types.deleteAccount.start)
|
||||||
|
.flatMap(() => {
|
||||||
|
const { csrfToken: _csrf } = getState().app;
|
||||||
|
return postJSON$('/account/delete', { _csrf })
|
||||||
|
.map(deleteAccountComplete)
|
||||||
|
.catch(doActionOnError(error => deleteAccountError(error)));
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteComplete = actions$::ofType(types.deleteAccount.complete)
|
||||||
|
.map(() => hardGoTo('/'));
|
||||||
|
|
||||||
|
const deleteError = actions$::ofType(types.deleteAccount.error)
|
||||||
|
.flatMap(createErrorObservable);
|
||||||
|
|
||||||
|
return Observable.merge(
|
||||||
|
resetStart,
|
||||||
|
resetError,
|
||||||
|
deleteStart,
|
||||||
|
deleteComplete,
|
||||||
|
deleteError
|
||||||
|
).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default dangerZoneEpic;
|
@ -1,23 +1,46 @@
|
|||||||
import { isLocationAction } from 'redux-first-router';
|
import { isLocationAction } from 'redux-first-router';
|
||||||
import {
|
import {
|
||||||
addNS,
|
composeReducers,
|
||||||
createAction,
|
createAction,
|
||||||
createAsyncTypes,
|
createAsyncTypes,
|
||||||
createTypes
|
createTypes,
|
||||||
|
handleActions
|
||||||
} from 'berkeleys-redux-utils';
|
} from 'berkeleys-redux-utils';
|
||||||
|
import { identity } from 'lodash';
|
||||||
|
|
||||||
import userUpdateEpic from './update-user-epic.js';
|
import certificateEpic from './certificate-epic';
|
||||||
|
import dangerZoneEpic from './danger-zone-epic';
|
||||||
|
import userUpdateEpic from './update-user-epic';
|
||||||
|
import newUsernameEpic from './new-username-epic';
|
||||||
import ns from '../ns.json';
|
import ns from '../ns.json';
|
||||||
import { utils } from '../../../Flash/redux';
|
import { utils } from '../../../Flash/redux';
|
||||||
|
|
||||||
export const epics = [
|
export const epics = [
|
||||||
|
certificateEpic,
|
||||||
|
dangerZoneEpic,
|
||||||
|
newUsernameEpic,
|
||||||
userUpdateEpic
|
userUpdateEpic
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const createActionWithFlash = type => createAction(
|
||||||
|
type,
|
||||||
|
null,
|
||||||
|
utils.createFlashMetaAction
|
||||||
|
);
|
||||||
|
|
||||||
export const types = createTypes([
|
export const types = createTypes([
|
||||||
'toggleUserFlag',
|
createAsyncTypes('claimCert'),
|
||||||
createAsyncTypes('updateMyEmail'),
|
createAsyncTypes('updateMyEmail'),
|
||||||
|
createAsyncTypes('updateUserBackend'),
|
||||||
|
createAsyncTypes('deletePortfolio'),
|
||||||
|
createAsyncTypes('updateMyPortfolio'),
|
||||||
'updateMyLang',
|
'updateMyLang',
|
||||||
|
'updateNewUsernameValidity',
|
||||||
|
createAsyncTypes('validateUsername'),
|
||||||
|
createAsyncTypes('refetchChallengeMap'),
|
||||||
|
createAsyncTypes('deleteAccount'),
|
||||||
|
createAsyncTypes('resetProgress'),
|
||||||
|
|
||||||
'onRouteSettings',
|
'onRouteSettings',
|
||||||
'onRouteUpdateEmail'
|
'onRouteUpdateEmail'
|
||||||
], 'settings');
|
], 'settings');
|
||||||
@ -25,34 +48,92 @@ export const types = createTypes([
|
|||||||
|
|
||||||
export const onRouteSettings = createAction(types.onRouteSettings);
|
export const onRouteSettings = createAction(types.onRouteSettings);
|
||||||
export const onRouteUpdateEmail = createAction(types.onRouteUpdateEmail);
|
export const onRouteUpdateEmail = createAction(types.onRouteUpdateEmail);
|
||||||
export const toggleUserFlag = createAction(types.toggleUserFlag);
|
|
||||||
|
export const claimCert = createAction(types.claimCert.start);
|
||||||
|
export const claimCertComplete = createAction(
|
||||||
|
types.claimCert.complete,
|
||||||
|
({ result }) => result,
|
||||||
|
identity
|
||||||
|
);
|
||||||
|
export const claimCertError = createAction(
|
||||||
|
types.claimCert.error,
|
||||||
|
identity
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateUserBackend = createAction(types.updateUserBackend.start);
|
||||||
|
export const updateUserBackendComplete = createActionWithFlash(
|
||||||
|
types.updateUserBackend.complete
|
||||||
|
);
|
||||||
|
export const updateUserBackendError = createActionWithFlash(
|
||||||
|
types.updateUserBackend.error
|
||||||
|
);
|
||||||
|
|
||||||
export const updateMyEmail = createAction(types.updateMyEmail.start);
|
export const updateMyEmail = createAction(types.updateMyEmail.start);
|
||||||
export const updateMyEmailComplete = createAction(
|
export const updateMyEmailComplete = createActionWithFlash(
|
||||||
types.updateMyEmail.complete,
|
types.updateMyEmail.complete
|
||||||
null,
|
);
|
||||||
utils.createFlashMetaAction
|
export const updateMyEmailError = createActionWithFlash(
|
||||||
|
types.updateMyEmail.error
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateMyEmailError = createAction(
|
export const updateMyPortfolio = createAction(types.updateMyPortfolio.start);
|
||||||
types.updateMyEmail.error,
|
export const updateMyPortfolioComplete = createAction(
|
||||||
null,
|
types.updateMyPortfolio.complete
|
||||||
utils.createFlashMetaAction
|
|
||||||
);
|
);
|
||||||
|
export const updateMyPortfolioError = createAction(
|
||||||
|
types.updateMyPortfolio.error
|
||||||
|
);
|
||||||
|
export const deletePortfolio = createAction(types.deletePortfolio.start);
|
||||||
|
export const deletePortfolioError = createAction(types.deletePortfolio.error);
|
||||||
export const updateMyLang = createAction(
|
export const updateMyLang = createAction(
|
||||||
types.updateMyLang,
|
types.updateMyLang,
|
||||||
(values) => values.lang
|
(values) => values.lang
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const resetProgress = createAction(types.resetProgress.start);
|
||||||
|
export const resetProgressComplete = createAction(types.resetProgress.complete);
|
||||||
|
export const resetProgressError = createAction(
|
||||||
|
types.resetProgress.error,
|
||||||
|
identity
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteAccount = createAction(types.deleteAccount.start);
|
||||||
|
export const deleteAccountComplete = createAction(types.deleteAccount.complete);
|
||||||
|
export const deleteAccountError = createAction(
|
||||||
|
types.deleteAccount.error,
|
||||||
|
identity
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateNewUsernameValidity = createAction(
|
||||||
|
types.updateNewUsernameValidity
|
||||||
|
);
|
||||||
|
|
||||||
|
export const refetchChallengeMap = createAction(
|
||||||
|
types.refetchChallengeMap.start
|
||||||
|
);
|
||||||
|
|
||||||
|
export const validateUsername = createAction(types.validateUsername.start);
|
||||||
|
export const validateUsernameError = createAction(
|
||||||
|
types.validateUsername.error,
|
||||||
|
identity
|
||||||
|
);
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
showUpdateEmailView: false
|
showUpdateEmailView: false,
|
||||||
|
isValidUsername: false,
|
||||||
|
validating: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNS = state => state[ns];
|
const getNS = state => state[ns];
|
||||||
|
|
||||||
|
export function settingsSelector(state) {
|
||||||
|
return getNS(state);
|
||||||
|
}
|
||||||
|
|
||||||
export const showUpdateEmailViewSelector =
|
export const showUpdateEmailViewSelector =
|
||||||
state => getNS(state).showUpdateEmailView;
|
state => getNS(state).showUpdateEmailView;
|
||||||
|
|
||||||
export default addNS(
|
export default composeReducers(
|
||||||
ns,
|
ns,
|
||||||
function settingsRouteReducer(state = defaultState, action) {
|
function settingsRouteReducer(state = defaultState, action) {
|
||||||
if (isLocationAction(action)) {
|
if (isLocationAction(action)) {
|
||||||
@ -71,5 +152,20 @@ export default addNS(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
},
|
||||||
|
handleActions(() => ({
|
||||||
|
[types.updateNewUsernameValidity]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
isValidUsername: payload,
|
||||||
|
validating: false
|
||||||
|
}),
|
||||||
|
[types.validateUsername.start]: state => ({
|
||||||
|
...state,
|
||||||
|
isValidUsername: false,
|
||||||
|
validating: true
|
||||||
|
}),
|
||||||
|
[types.validateUsername.error]: state => ({ ...state, validating: false })
|
||||||
|
}),
|
||||||
|
defaultState
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
30
common/app/routes/Settings/redux/new-username-epic.js
Normal file
30
common/app/routes/Settings/redux/new-username-epic.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
import { ofType } from 'redux-epic';
|
||||||
|
|
||||||
|
import {
|
||||||
|
types,
|
||||||
|
updateNewUsernameValidity,
|
||||||
|
validateUsernameError
|
||||||
|
} from './';
|
||||||
|
import { getJSON$ } from '../../../../utils/ajax-stream';
|
||||||
|
import {
|
||||||
|
doActionOnError,
|
||||||
|
createErrorObservable
|
||||||
|
} from '../../../redux';
|
||||||
|
|
||||||
|
function validateUsernameEpic(actions$) {
|
||||||
|
const start = actions$::ofType(types.validateUsername.start)
|
||||||
|
.debounce(500)
|
||||||
|
.flatMap(({ payload }) =>
|
||||||
|
getJSON$(`/api/users/exists?username=${payload}`)
|
||||||
|
.map(({ exists }) => updateNewUsernameValidity(!exists))
|
||||||
|
.catch(error => doActionOnError(() => validateUsernameError(error)))
|
||||||
|
);
|
||||||
|
|
||||||
|
const error = actions$::ofType(types.validateUsername.error)
|
||||||
|
.flatMap(createErrorObservable);
|
||||||
|
|
||||||
|
return Observable.merge(start, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default validateUsernameEpic;
|
@ -1,31 +1,176 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import { combineEpics, ofType } from 'redux-epic';
|
import { combineEpics, ofType } from 'redux-epic';
|
||||||
|
import { pick } from 'lodash';
|
||||||
import { types, onRouteSettings } from './';
|
import {
|
||||||
|
types,
|
||||||
|
onRouteSettings,
|
||||||
|
refetchChallengeMap,
|
||||||
|
updateUserBackendComplete,
|
||||||
|
updateMyPortfolioComplete
|
||||||
|
} from './';
|
||||||
import { makeToast } from '../../../Toasts/redux';
|
import { makeToast } from '../../../Toasts/redux';
|
||||||
import {
|
import {
|
||||||
fetchChallenges,
|
updateChallenges,
|
||||||
doActionOnError,
|
doActionOnError,
|
||||||
userSelector
|
usernameSelector,
|
||||||
|
userSelector,
|
||||||
|
createErrorObservable
|
||||||
} from '../../../redux';
|
} from '../../../redux';
|
||||||
import {
|
import {
|
||||||
updateUserFlag,
|
|
||||||
updateUserEmail,
|
updateUserEmail,
|
||||||
updateUserLang
|
updateUserLang,
|
||||||
|
updateMultipleUserFlags,
|
||||||
|
regresPortfolio,
|
||||||
|
optoUpdatePortfolio
|
||||||
} from '../../../entities';
|
} from '../../../entities';
|
||||||
|
|
||||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
import langs from '../../../../utils/supported-languages';
|
import langs from '../../../../utils/supported-languages';
|
||||||
|
|
||||||
const urlMap = {
|
const endpoints = {
|
||||||
isLocked: 'lockdown',
|
email: '/update-my-email',
|
||||||
isAvailableForHire: 'available-for-hire',
|
projects: '/update-my-projects',
|
||||||
sendQuincyEmail: 'quincy-email',
|
username: '/update-my-username'
|
||||||
sendNotificationEmail: 'notification-email',
|
|
||||||
sendMonthlyEmail: 'announcement-email'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function updateUserEmailEpic(actions, { getState }) {
|
function backendUserUpdateEpic(actions$, { getState }) {
|
||||||
|
const start = actions$::ofType(types.updateUserBackend.start);
|
||||||
|
const server = start
|
||||||
|
.flatMap(({ payload }) => {
|
||||||
|
const userMap = userSelector(getState());
|
||||||
|
const { username } = userMap;
|
||||||
|
const flagsToCheck = Object.keys(payload);
|
||||||
|
const valuesToCheck = pick(userMap, flagsToCheck);
|
||||||
|
const oldValues = {
|
||||||
|
...flagsToCheck.reduce((accu, current) => ({ ...accu, [current]: '' })),
|
||||||
|
...valuesToCheck
|
||||||
|
};
|
||||||
|
const valuesToUpdate = flagsToCheck.reduce((accu, current) => {
|
||||||
|
if (payload[current] !== valuesToCheck[current]) {
|
||||||
|
return { ...accu, [current]: payload[current] };
|
||||||
|
}
|
||||||
|
return accu;
|
||||||
|
}, {});
|
||||||
|
if (!Object.keys(valuesToUpdate).length) {
|
||||||
|
return Observable.of(
|
||||||
|
makeToast({ message: 'No changes in settings detected' })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
app: { csrfToken: _csrf }
|
||||||
|
} = getState();
|
||||||
|
let body = { _csrf };
|
||||||
|
let endpoint = '/update-flags';
|
||||||
|
const updateKeys = Object.keys(valuesToUpdate);
|
||||||
|
if (updateKeys.length === 1 && updateKeys[0] in endpoints) {
|
||||||
|
// there is a specific route for this update
|
||||||
|
const flag = updateKeys[0];
|
||||||
|
endpoint = endpoints[flag];
|
||||||
|
body = {
|
||||||
|
...body,
|
||||||
|
[flag]: valuesToUpdate[flag]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
body = {
|
||||||
|
...body,
|
||||||
|
values: valuesToUpdate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return postJSON$(endpoint, body)
|
||||||
|
.map(updateUserBackendComplete)
|
||||||
|
.catch(
|
||||||
|
doActionOnError(
|
||||||
|
() => updateMultipleUserFlags({ username, flags: oldValues })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const optimistic = start
|
||||||
|
.flatMap(({ payload }) => {
|
||||||
|
const username = usernameSelector(getState());
|
||||||
|
return Observable.of(
|
||||||
|
updateMultipleUserFlags({ username, flags: payload })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const complete = actions$::ofType(types.updateUserBackend.complete)
|
||||||
|
.flatMap(({ payload: { message } }) => Observable.if(
|
||||||
|
() => message.includes('project'),
|
||||||
|
Observable.of(refetchChallengeMap(), makeToast({ message })),
|
||||||
|
Observable.of(makeToast({ message }))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Observable.merge(server, optimistic, complete);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refetchChallengeMapEpic(actions$, { getState }) {
|
||||||
|
return actions$::ofType(types.refetchChallengeMap.start)
|
||||||
|
.flatMap(() => {
|
||||||
|
const {
|
||||||
|
app: { csrfToken: _csrf }
|
||||||
|
} = getState();
|
||||||
|
const username = usernameSelector(getState());
|
||||||
|
return postJSON$('/refetch-user-challenge-map', { _csrf })
|
||||||
|
.map(({ challengeMap }) =>
|
||||||
|
updateMultipleUserFlags({ username, flags: { challengeMap } })
|
||||||
|
)
|
||||||
|
.catch(createErrorObservable);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMyPortfolioEpic(actions$, { getState }) {
|
||||||
|
const edit = actions$::ofType(types.updateMyPortfolio.start);
|
||||||
|
const remove = actions$::ofType(types.deletePortfolio.start);
|
||||||
|
const serverEdit = edit
|
||||||
|
.flatMap(({ payload }) => {
|
||||||
|
const { id } = payload;
|
||||||
|
const {
|
||||||
|
app: { csrfToken: _csrf, username }
|
||||||
|
} = getState();
|
||||||
|
return postJSON$('/update-my-portfolio', { _csrf, portfolio: payload })
|
||||||
|
.map(updateMyPortfolioComplete)
|
||||||
|
.catch(doActionOnError(() => regresPortfolio({ username, id })));
|
||||||
|
});
|
||||||
|
const optimisticEdit = edit
|
||||||
|
.map(({ payload }) => {
|
||||||
|
const username = usernameSelector(getState());
|
||||||
|
return optoUpdatePortfolio({ username, portfolio: payload });
|
||||||
|
});
|
||||||
|
const complete = actions$::ofType(types.updateMyPortfolio.complete)
|
||||||
|
.flatMap(({ payload: { message } }) =>
|
||||||
|
Observable.of(makeToast({ message }))
|
||||||
|
);
|
||||||
|
|
||||||
|
const serverRemove = remove
|
||||||
|
.flatMap(({ payload: { portfolio } }) => {
|
||||||
|
const {
|
||||||
|
app: { csrfToken: _csrf }
|
||||||
|
} = getState();
|
||||||
|
return postJSON$('/update-my-portfolio', { _csrf, portfolio })
|
||||||
|
.map(updateMyPortfolioComplete)
|
||||||
|
.catch(
|
||||||
|
doActionOnError(
|
||||||
|
() => makeToast({
|
||||||
|
message: 'Something went wrong removing a portfolio item.'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const optimisticRemove = remove
|
||||||
|
.flatMap(({ payload: { portfolio: { id } } }) => {
|
||||||
|
const username = usernameSelector(getState());
|
||||||
|
return Observable.of(regresPortfolio({ username, id }));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Observable.merge(
|
||||||
|
serverEdit,
|
||||||
|
optimisticEdit,
|
||||||
|
complete,
|
||||||
|
serverRemove,
|
||||||
|
optimisticRemove
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUserEmailEpic(actions, { getState }) {
|
||||||
return actions::ofType(types.updateMyEmail)
|
return actions::ofType(types.updateMyEmail)
|
||||||
.flatMap(({ payload: email }) => {
|
.flatMap(({ payload: email }) => {
|
||||||
const {
|
const {
|
||||||
@ -38,7 +183,6 @@ export function updateUserEmailEpic(actions, { getState }) {
|
|||||||
updateUserEmail(username, email)
|
updateUserEmail(username, email)
|
||||||
);
|
);
|
||||||
const ajaxUpdate = postJSON$('/update-my-email', body)
|
const ajaxUpdate = postJSON$('/update-my-email', body)
|
||||||
.map(({ message }) => makeToast({ message }))
|
|
||||||
.catch(doActionOnError(() => oldEmail ?
|
.catch(doActionOnError(() => oldEmail ?
|
||||||
updateUserEmail(username, oldEmail) :
|
updateUserEmail(username, oldEmail) :
|
||||||
null
|
null
|
||||||
@ -71,7 +215,7 @@ export function updateUserLangEpic(actions, { getState }) {
|
|||||||
// update url to reflect change
|
// update url to reflect change
|
||||||
onRouteSettings({ lang }),
|
onRouteSettings({ lang }),
|
||||||
// refetch challenges in new language
|
// refetch challenges in new language
|
||||||
fetchChallenges()
|
updateChallenges()
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(doActionOnError(() => {
|
.catch(doActionOnError(() => {
|
||||||
@ -85,41 +229,11 @@ export function updateUserLangEpic(actions, { getState }) {
|
|||||||
});
|
});
|
||||||
return Observable.merge(ajaxUpdate, optimistic);
|
return Observable.merge(ajaxUpdate, optimistic);
|
||||||
}
|
}
|
||||||
export function updateUserFlagEpic(actions, { getState }) {
|
|
||||||
const toggleFlag = actions
|
|
||||||
.filter(({ type, payload }) => type === types.toggleUserFlag && payload)
|
|
||||||
.map(({ payload }) => payload);
|
|
||||||
const optimistic = toggleFlag.map(flag => {
|
|
||||||
const { app: { user: username } } = getState();
|
|
||||||
return updateUserFlag(username, flag);
|
|
||||||
});
|
|
||||||
const serverUpdate = toggleFlag
|
|
||||||
.debounce(500)
|
|
||||||
.flatMap(flag => {
|
|
||||||
const url = `/toggle-${urlMap[ flag ]}`;
|
|
||||||
const {
|
|
||||||
app: { user: username, csrfToken: _csrf },
|
|
||||||
entities: { user: userMap }
|
|
||||||
} = getState();
|
|
||||||
const user = userMap[username];
|
|
||||||
const currentValue = user[ flag ];
|
|
||||||
return postJSON$(url, { _csrf })
|
|
||||||
.map(({ flag, value }) => {
|
|
||||||
if (currentValue === value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return updateUserFlag(username, flag);
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.catch(doActionOnError(() => {
|
|
||||||
return updateUserFlag(username, currentValue);
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
return Observable.merge(optimistic, serverUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default combineEpics(
|
export default combineEpics(
|
||||||
updateUserFlagEpic,
|
backendUserUpdateEpic,
|
||||||
|
refetchChallengeMapEpic,
|
||||||
|
updateMyPortfolioEpic,
|
||||||
updateUserEmailEpic,
|
updateUserEmailEpic,
|
||||||
updateUserLangEpic
|
updateUserLangEpic
|
||||||
);
|
);
|
||||||
|
@ -3,20 +3,46 @@
|
|||||||
|
|
||||||
@skeleton-gray: #b0bdb7;
|
@skeleton-gray: #b0bdb7;
|
||||||
|
|
||||||
@keyframes pulsingOverlay {
|
.night .@{ns}-container {
|
||||||
0% {
|
|
||||||
opacity: 0.5;
|
.btn-group {
|
||||||
}
|
|
||||||
50% {
|
label:disabled, label[disabled] {
|
||||||
opacity: 0.8;
|
border: 1px solid #999;
|
||||||
}
|
background-color: #999;
|
||||||
100% {
|
color: #333;
|
||||||
opacity: 0.5;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{ns}-container {
|
.@{ns}-container {
|
||||||
.center(@value: @container-xl, @padding: @grid-gutter-width);
|
.center(@value: @container-xl, @padding: @grid-gutter-width);
|
||||||
|
|
||||||
|
button:disabled, button[disabled] {
|
||||||
|
border: 1px solid #999;
|
||||||
|
background-color: #999;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solution-list {
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solution-list-item {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{ns}-email-container {
|
.@{ns}-email-container {
|
||||||
@ -25,23 +51,41 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{ns}-skeleton {
|
.inline-form-field {
|
||||||
background-color: #fff;
|
display: flex;
|
||||||
z-index: 10;
|
align-items: center;
|
||||||
animation-name: pulsingOverlay;
|
margin: 5px 0;
|
||||||
animation-duration: 2.5s;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-direction: normal;
|
|
||||||
|
|
||||||
.placeholder-string {
|
input, textarea {
|
||||||
background-color: @skeleton-gray;
|
background-color: #fff;
|
||||||
box-shadow: 0px 0px 12px 6px @skeleton-gray;
|
}
|
||||||
color: @skeleton-gray;
|
}
|
||||||
}
|
.edit-preview-tabs {
|
||||||
.btn-link-social {
|
|
||||||
background-color: @skeleton-gray;
|
li {
|
||||||
border-color: @skeleton-gray;
|
padding-bottom: 0px;
|
||||||
box-shadow: 0px 0px 12px 6px @skeleton-gray;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-settings-intro {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
.p-intro {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone-panel {
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.panel-heading {
|
||||||
|
background-color: #880000;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
common/app/routes/Settings/utils/buildUserProjectsMap.js
Normal file
32
common/app/routes/Settings/utils/buildUserProjectsMap.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { dasherize } from '../../../../../server/utils/index';
|
||||||
|
|
||||||
|
export const jsProjectSuperBlock = 'javascript-algorithms-and-data-structures';
|
||||||
|
|
||||||
|
export function buildUserProjectsMap(projectBlock, challengeMap) {
|
||||||
|
const {
|
||||||
|
challengeNameIdMap,
|
||||||
|
challenges,
|
||||||
|
superBlock
|
||||||
|
} = projectBlock;
|
||||||
|
return {
|
||||||
|
[superBlock]: challenges.reduce((solutions, current) => {
|
||||||
|
const dashedName = dasherize(current)
|
||||||
|
.replace('java-script', 'javascript')
|
||||||
|
.replace('metric-imperial', 'metricimperial');
|
||||||
|
const completed = challengeMap[challengeNameIdMap[dashedName]];
|
||||||
|
let solution = '';
|
||||||
|
if (superBlock === jsProjectSuperBlock) {
|
||||||
|
solution = {};
|
||||||
|
}
|
||||||
|
if (completed) {
|
||||||
|
solution = 'solution' in completed ?
|
||||||
|
completed.solution :
|
||||||
|
completed.files;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...solutions,
|
||||||
|
[current]: solution
|
||||||
|
};
|
||||||
|
}, {})
|
||||||
|
};
|
||||||
|
}
|
@ -6,17 +6,16 @@ import debugFactory from 'debug';
|
|||||||
import { isEmail } from 'validator';
|
import { isEmail } from 'validator';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import loopback from 'loopback';
|
import loopback from 'loopback';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { themes } from '../utils/themes';
|
import { themes } from '../utils/themes';
|
||||||
|
import { dasherize } from '../../server/utils';
|
||||||
import { saveUser, observeMethod } from '../../server/utils/rx.js';
|
import { saveUser, observeMethod } from '../../server/utils/rx.js';
|
||||||
import { blacklistedUsernames } from '../../server/utils/constants.js';
|
import { blacklistedUsernames } from '../../server/utils/constants.js';
|
||||||
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
|
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
|
||||||
import {
|
import {
|
||||||
getServerFullURL,
|
getServerFullURL,
|
||||||
getEmailSender,
|
getEmailSender
|
||||||
getProtocol,
|
|
||||||
getHost,
|
|
||||||
getPort
|
|
||||||
} from '../../server/utils/url-utils.js';
|
} from '../../server/utils/url-utils.js';
|
||||||
|
|
||||||
const debug = debugFactory('fcc:models:user');
|
const debug = debugFactory('fcc:models:user');
|
||||||
@ -38,6 +37,61 @@ function destroyAll(id, Model) {
|
|||||||
)({ userId: id });
|
)({ userId: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildChallengeMapUpdate(challengeMap, project) {
|
||||||
|
const currentChallengeMap = { ...challengeMap };
|
||||||
|
const { nameToIdMap } = _.values(project)[0];
|
||||||
|
const incomingUpdate = _.pickBy(
|
||||||
|
_.omit(_.values(project)[0], [ 'id', 'nameToIdMap' ]),
|
||||||
|
Boolean
|
||||||
|
);
|
||||||
|
const currentCompletedProjects = _.pick(challengeMap, _.values(nameToIdMap));
|
||||||
|
const now = Date.now();
|
||||||
|
const update = Object.keys(incomingUpdate).reduce((update, current) => {
|
||||||
|
const dashedName = dasherize(current)
|
||||||
|
.replace('java-script', 'javascript')
|
||||||
|
.replace('metric-imperial', 'metricimperial');
|
||||||
|
const currentId = nameToIdMap[dashedName];
|
||||||
|
if (
|
||||||
|
currentId in currentCompletedProjects &&
|
||||||
|
currentCompletedProjects[currentId].solution !== incomingUpdate[current]
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...update,
|
||||||
|
[currentId]: {
|
||||||
|
...currentCompletedProjects[currentId],
|
||||||
|
solution: incomingUpdate[current],
|
||||||
|
numOfAttempts: currentCompletedProjects[currentId].numOfAttempts + 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!(currentId in currentCompletedProjects)) {
|
||||||
|
return {
|
||||||
|
...update,
|
||||||
|
[currentId]: {
|
||||||
|
id: currentId,
|
||||||
|
solution: incomingUpdate[current],
|
||||||
|
challengeType: 3,
|
||||||
|
completedDate: now,
|
||||||
|
numOfAttempts: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return update;
|
||||||
|
}, {});
|
||||||
|
const updatedExisting = {
|
||||||
|
...currentCompletedProjects,
|
||||||
|
...update
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...currentChallengeMap,
|
||||||
|
...updatedExisting
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTheSame(val1, val2) {
|
||||||
|
return val1 === val2;
|
||||||
|
}
|
||||||
|
|
||||||
const renderSignUpEmail = loopback.template(path.join(
|
const renderSignUpEmail = loopback.template(path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
'..',
|
||||||
@ -58,6 +112,16 @@ const renderSignInEmail = loopback.template(path.join(
|
|||||||
'user-request-sign-in.ejs'
|
'user-request-sign-in.ejs'
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const renderEmailChangeEmail = loopback.template(path.join(
|
||||||
|
__dirname,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'server',
|
||||||
|
'views',
|
||||||
|
'emails',
|
||||||
|
'user-request-update-email.ejs'
|
||||||
|
));
|
||||||
|
|
||||||
function getAboutProfile({
|
function getAboutProfile({
|
||||||
username,
|
username,
|
||||||
githubProfile: github,
|
githubProfile: github,
|
||||||
@ -285,7 +349,12 @@ module.exports = function(User) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
User.prototype.loginByRequest = function login(req, res) {
|
User.prototype.loginByRequest = function loginByRequest(req, res) {
|
||||||
|
const {
|
||||||
|
query: {
|
||||||
|
emailChange
|
||||||
|
}
|
||||||
|
} = req;
|
||||||
const createToken = this.createAccessToken$()
|
const createToken = this.createAccessToken$()
|
||||||
.do(accessToken => {
|
.do(accessToken => {
|
||||||
const config = {
|
const config = {
|
||||||
@ -297,11 +366,19 @@ module.exports = function(User) {
|
|||||||
res.cookie('userId', accessToken.userId, config);
|
res.cookie('userId', accessToken.userId, config);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const updateUser = this.update$({
|
let data = {
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
emailAuthLinkTTL: null,
|
emailAuthLinkTTL: null,
|
||||||
emailVerifyTTL: null
|
emailVerifyTTL: null
|
||||||
});
|
};
|
||||||
|
if (emailChange && this.newEmail) {
|
||||||
|
data = {
|
||||||
|
...data,
|
||||||
|
email: this.newEmail,
|
||||||
|
newEmail: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const updateUser = this.update$(data);
|
||||||
return Observable.combineLatest(
|
return Observable.combineLatest(
|
||||||
createToken,
|
createToken,
|
||||||
updateUser,
|
updateUser,
|
||||||
@ -425,7 +502,7 @@ module.exports = function(User) {
|
|||||||
|
|
||||||
User.decodeEmail = email => Buffer(email, 'base64').toString();
|
User.decodeEmail = email => Buffer(email, 'base64').toString();
|
||||||
|
|
||||||
User.prototype.requestAuthEmail = function requestAuthEmail(isSignUp) {
|
function requestAuthEmail(isSignUp, newEmail) {
|
||||||
return Observable.defer(() => {
|
return Observable.defer(() => {
|
||||||
const messageOrNull = getWaitMessage(this.emailAuthLinkTTL);
|
const messageOrNull = getWaitMessage(this.emailAuthLinkTTL);
|
||||||
if (messageOrNull) {
|
if (messageOrNull) {
|
||||||
@ -448,22 +525,26 @@ module.exports = function(User) {
|
|||||||
renderAuthEmail = renderSignUpEmail;
|
renderAuthEmail = renderSignUpEmail;
|
||||||
subject = 'Account Created - freeCodeCamp';
|
subject = 'Account Created - freeCodeCamp';
|
||||||
}
|
}
|
||||||
|
if (newEmail) {
|
||||||
|
renderAuthEmail = renderEmailChangeEmail;
|
||||||
|
subject = 'Email Change Request - freeCodeCamp';
|
||||||
|
}
|
||||||
const { id: loginToken, created: emailAuthLinkTTL } = token;
|
const { id: loginToken, created: emailAuthLinkTTL } = token;
|
||||||
const loginEmail = this.getEncodedEmail();
|
const loginEmail = this.getEncodedEmail(newEmail ? newEmail : null);
|
||||||
const host = getServerFullURL();
|
const host = getServerFullURL();
|
||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
type: 'email',
|
type: 'email',
|
||||||
to: this.email,
|
to: newEmail ? newEmail : this.email,
|
||||||
from: getEmailSender(),
|
from: getEmailSender(),
|
||||||
subject,
|
subject,
|
||||||
text: renderAuthEmail({
|
text: renderAuthEmail({
|
||||||
host,
|
host,
|
||||||
loginEmail,
|
loginEmail,
|
||||||
loginToken
|
loginToken,
|
||||||
|
emailChange: !!newEmail
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
return Observable.forkJoin(
|
||||||
return Observable.combineLatest(
|
|
||||||
User.email.send$(mailOptions),
|
User.email.send$(mailOptions),
|
||||||
this.update$({ emailAuthLinkTTL })
|
this.update$({ emailAuthLinkTTL })
|
||||||
);
|
);
|
||||||
@ -479,17 +560,19 @@ module.exports = function(User) {
|
|||||||
Please follow that link to sign in.
|
Please follow that link to sign in.
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
User.prototype.requestAuthEmail = requestAuthEmail;
|
||||||
|
|
||||||
User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) {
|
User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) {
|
||||||
|
const currentEmail = this.email;
|
||||||
return Observable.defer(() => {
|
return Observable.defer(() => {
|
||||||
const ownEmail = newEmail === this.email;
|
const isOwnEmail = isTheSame(newEmail, currentEmail);
|
||||||
if (!isEmail('' + newEmail)) {
|
const sameUpdate = isTheSame(newEmail, this.newEmail);
|
||||||
throw createEmailError();
|
const messageOrNull = getWaitMessage(this.emailVerifyTTL);
|
||||||
}
|
if (isOwnEmail) {
|
||||||
// email is already associated and verified with this account
|
|
||||||
if (ownEmail) {
|
|
||||||
if (this.emailVerified) {
|
if (this.emailVerified) {
|
||||||
|
// email is already associated and verified with this account
|
||||||
throw wrapHandledError(
|
throw wrapHandledError(
|
||||||
new Error('email is already verified'),
|
new Error('email is already verified'),
|
||||||
{
|
{
|
||||||
@ -497,10 +580,8 @@ module.exports = function(User) {
|
|||||||
message: `${newEmail} is already associated with this account.`
|
message: `${newEmail} is already associated with this account.`
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else if (!this.emailVerified && messageOrNull) {
|
||||||
const messageOrNull = getWaitMessage(this.emailVerifyTTL);
|
// email is associated but unverified and
|
||||||
// email is already associated but unverified
|
|
||||||
if (messageOrNull) {
|
|
||||||
// email is within time limit
|
// email is within time limit
|
||||||
throw wrapHandledError(
|
throw wrapHandledError(
|
||||||
new Error(),
|
new Error(),
|
||||||
@ -510,69 +591,175 @@ module.exports = function(User) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (sameUpdate && messageOrNull) {
|
||||||
// at this point email is not associated with the account
|
// trying to update with the same newEmail and
|
||||||
// or has not been verified but user is requesting another token
|
// confirmation email is still valid
|
||||||
// outside of the time limit
|
throw wrapHandledError(
|
||||||
|
new Error(),
|
||||||
|
{
|
||||||
|
type: 'info',
|
||||||
|
message: dedent`
|
||||||
|
We have already sent an email change request to ${newEmail}.
|
||||||
|
Please check your inbox`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!isEmail('' + newEmail)) {
|
||||||
|
throw createEmailError();
|
||||||
|
}
|
||||||
|
// newEmail is not associated with this user, and
|
||||||
|
// this attempt to change email is the first or
|
||||||
|
// previous attempts have expired
|
||||||
return Observable.if(
|
return Observable.if(
|
||||||
() => ownEmail,
|
() => isOwnEmail || (sameUpdate && messageOrNull),
|
||||||
Observable.empty(),
|
Observable.empty(),
|
||||||
// defer prevents the promise from firing prematurely (before subscribe)
|
// defer prevents the promise from firing prematurely (before subscribe)
|
||||||
Observable.defer(() => User.doesExist(null, newEmail))
|
Observable.defer(() => User.doesExist(null, newEmail))
|
||||||
)
|
)
|
||||||
.do(exists => {
|
.do(exists => {
|
||||||
// not associated with this account, but is associated with another
|
if (exists) {
|
||||||
if (exists) {
|
// newEmail is not associated with this account,
|
||||||
throw wrapHandledError(
|
// but is associated with different account
|
||||||
new Error('email already in use'),
|
throw wrapHandledError(
|
||||||
{
|
new Error('email already in use'),
|
||||||
type: 'info',
|
{
|
||||||
message:
|
type: 'info',
|
||||||
`${newEmail} is already associated with another account.`
|
message:
|
||||||
}
|
`${newEmail} is already associated with another account.`
|
||||||
);
|
}
|
||||||
}
|
);
|
||||||
})
|
}
|
||||||
.defaultIfEmpty();
|
|
||||||
})
|
|
||||||
.flatMap(() => {
|
|
||||||
const emailVerified = false;
|
|
||||||
const data = {
|
|
||||||
newEmail,
|
|
||||||
emailVerified,
|
|
||||||
emailVerifyTTL: new Date()
|
|
||||||
};
|
|
||||||
return this.update$(data).do(() => Object.assign(this, data));
|
|
||||||
})
|
})
|
||||||
.flatMap(() => {
|
.flatMap(() => {
|
||||||
const mailOptions = {
|
const update = {
|
||||||
type: 'email',
|
newEmail,
|
||||||
to: newEmail,
|
emailVerified: false,
|
||||||
from: getEmailSender(),
|
emailVerifyTTL: new Date()
|
||||||
subject: 'freeCodeCamp - Email Update Requested',
|
};
|
||||||
protocol: getProtocol(),
|
return this.update$(update)
|
||||||
host: getHost(),
|
.do(() => Object.assign(this, update))
|
||||||
port: getPort(),
|
.flatMap(() => this.requestAuthEmail(false, newEmail));
|
||||||
template: path.join(
|
});
|
||||||
__dirname,
|
});
|
||||||
'..',
|
};
|
||||||
'..',
|
|
||||||
'server',
|
User.prototype.requestChallengeMap = function requestChallengeMap() {
|
||||||
'views',
|
return this.getChallengeMap$();
|
||||||
'emails',
|
};
|
||||||
'user-request-update-email.ejs'
|
|
||||||
)
|
User.prototype.requestUpdateFlags = function requestUpdateFlags(values) {
|
||||||
};
|
const flagsToCheck = Object.keys(values);
|
||||||
return this.verify(mailOptions);
|
const valuesToCheck = _.pick({ ...this }, flagsToCheck);
|
||||||
|
const valuesToUpdate = flagsToCheck
|
||||||
|
.filter(flag => !isTheSame(values[flag], valuesToCheck[flag]));
|
||||||
|
if (!valuesToUpdate.length) {
|
||||||
|
return Observable.of(dedent`
|
||||||
|
No property in
|
||||||
|
${JSON.stringify(flagsToCheck, null, 2)}
|
||||||
|
will introduce a change in this user.
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.do(console.log)
|
||||||
|
.map(() => dedent`Your settings have not been changed`);
|
||||||
|
}
|
||||||
|
return Observable.from(valuesToUpdate)
|
||||||
|
.flatMap(flag => Observable.of({ flag, newValue: values[flag] }))
|
||||||
|
.toArray()
|
||||||
|
.flatMap(updates => {
|
||||||
|
return Observable.forkJoin(
|
||||||
|
Observable.from(updates)
|
||||||
|
.flatMap(({ flag, newValue }) => {
|
||||||
|
return Observable.fromPromise(User.doesExist(null, this.email))
|
||||||
|
.flatMap(() => {
|
||||||
|
return this.update$({ [flag]: newValue })
|
||||||
|
.do(() => {
|
||||||
|
this[flag] = newValue;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.map(() => dedent`
|
.map(() => dedent`
|
||||||
Please check your email.
|
We have successfully updated your account.
|
||||||
We sent you a link that you can click to verify your email address.
|
|
||||||
`);
|
`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
User.prototype.updateMyPortfolio =
|
||||||
|
function updateMyPortfolio(portfolioItem, deleteRequest) {
|
||||||
|
const currentPortfolio = this.portfolio.slice(0);
|
||||||
|
const pIndex = _.findIndex(
|
||||||
|
currentPortfolio,
|
||||||
|
p => p.id === portfolioItem.id
|
||||||
|
);
|
||||||
|
let updatedPortfolio = [];
|
||||||
|
if (deleteRequest) {
|
||||||
|
updatedPortfolio = currentPortfolio.filter(
|
||||||
|
p => p.id !== portfolioItem.id
|
||||||
|
);
|
||||||
|
} else if (pIndex === -1) {
|
||||||
|
updatedPortfolio = currentPortfolio.concat([ portfolioItem ]);
|
||||||
|
} else {
|
||||||
|
updatedPortfolio = [ ...currentPortfolio ];
|
||||||
|
updatedPortfolio[pIndex] = { ...portfolioItem };
|
||||||
|
}
|
||||||
|
return this.update$({ portfolio: updatedPortfolio })
|
||||||
|
.do(() => {
|
||||||
|
this.portfolio = updatedPortfolio;
|
||||||
|
})
|
||||||
|
.map(() => dedent`
|
||||||
|
Your portfolio has been updated
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
User.prototype.updateMyProjects = function updateMyProjects(project) {
|
||||||
|
const updateData = {};
|
||||||
|
return this.getChallengeMap$()
|
||||||
|
.flatMap(challengeMap => {
|
||||||
|
updateData.challengeMap = buildChallengeMapUpdate(
|
||||||
|
challengeMap,
|
||||||
|
project
|
||||||
|
);
|
||||||
|
return this.update$(updateData);
|
||||||
|
})
|
||||||
|
.do(() => Object.assign(this, updateData))
|
||||||
|
.map(() => dedent`
|
||||||
|
Your projects have been updated
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
User.prototype.updateMyUsername = function updateMyUsername(newUsername) {
|
||||||
|
return Observable.defer(
|
||||||
|
() => {
|
||||||
|
const isOwnUsername = isTheSame(newUsername, this.username);
|
||||||
|
if (isOwnUsername) {
|
||||||
|
return Observable.of(dedent`
|
||||||
|
${newUsername} is already associated with this account
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
return Observable.fromPromise(User.doesExist(newUsername));
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.flatMap(boolOrMessage => {
|
||||||
|
if (typeof boolOrMessage === 'string') {
|
||||||
|
return Observable.of(boolOrMessage);
|
||||||
|
}
|
||||||
|
if (boolOrMessage) {
|
||||||
|
return Observable.of(dedent`
|
||||||
|
${newUsername} is associated with a different account
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.update$({ username: newUsername })
|
||||||
|
.do(() => {
|
||||||
|
this.username = newUsername;
|
||||||
|
})
|
||||||
|
.map(() => dedent`
|
||||||
|
Username updated successfully
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
User.giveBrowniePoints =
|
User.giveBrowniePoints =
|
||||||
function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) {
|
function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) {
|
||||||
const findUser = observeMethod(User, 'findOne');
|
const findUser = observeMethod(User, 'findOne');
|
||||||
|
@ -89,6 +89,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"default": ""
|
"default": ""
|
||||||
},
|
},
|
||||||
|
"about": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": ""
|
"default": ""
|
||||||
@ -139,11 +143,6 @@
|
|||||||
"description": "Campers profile does not show challenges/certificates to the public",
|
"description": "Campers profile does not show challenges/certificates to the public",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
"isAvailableForHire": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Camper is available for hire",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"currentChallengeId": {
|
"currentChallengeId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The challenge last visited by the user",
|
"description": "The challenge last visited by the user",
|
||||||
@ -185,12 +184,12 @@
|
|||||||
},
|
},
|
||||||
"isRespWebDesignCert": {
|
"isRespWebDesignCert": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Camper is data visualization certified",
|
"description": "Camper is responsive web design certified",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
"isNewDataVisCert": {
|
"is2018DataVisCert": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Camper is responsive web design certified",
|
"description": "Camper is data visualization certified (2018)",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
"isFrontEndLibsCert": {
|
"isFrontEndLibsCert": {
|
||||||
@ -243,6 +242,10 @@
|
|||||||
],
|
],
|
||||||
"default": []
|
"default": []
|
||||||
},
|
},
|
||||||
|
"portfolio": {
|
||||||
|
"type": "array",
|
||||||
|
"default": []
|
||||||
|
},
|
||||||
"rand": {
|
"rand": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"index": true
|
"index": true
|
||||||
|
27
common/resource/academicPolicy.js
Normal file
27
common/resource/academicPolicy.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const policy = [
|
||||||
|
'Before you can claim a verified certificate, you must accept the ' +
|
||||||
|
'Academic Honesty Policy below.',
|
||||||
|
'I understand that plagiarism means copying someone else’s work and ' +
|
||||||
|
'presenting the work as if it were my own, without clearly attributing ' +
|
||||||
|
'the original author.',
|
||||||
|
'I understand that plagiarism is an act of intellectual dishonesty, and ' +
|
||||||
|
'that people usually get kicked out of university or fired from their ' +
|
||||||
|
'jobs if they get caught plagiarizing.',
|
||||||
|
'Aside from using open source libraries such as jQuery and Bootstrap, ' +
|
||||||
|
'and short snippets of code which are clearly attributed to their ' +
|
||||||
|
'original author, 100% of the code in my projects was written by me, or ' +
|
||||||
|
'along with another camper with whom I was pair programming in real time.',
|
||||||
|
'I pledge that I did not plagiarize any of my freeCodeCamp work. ' +
|
||||||
|
'I understand that freeCodeCamp’s team will audit my projects ' +
|
||||||
|
'to confirm this.',
|
||||||
|
'In the situations where we discover instances of unambiguous plagiarism, ' +
|
||||||
|
'we will replace the camper in question’s certification with a message ' +
|
||||||
|
'that "Upon review, this account has been flagged for academic dishonesty."',
|
||||||
|
'As an academic institution that grants achievement-based certifications, ' +
|
||||||
|
'we take academic honesty very seriously. If you have any questions about ' +
|
||||||
|
'this policy, or suspect that someone has violated it, you can email ' +
|
||||||
|
'<a href="mailto:team@freecodecamp.org">team@freecodecamp.org</a> and we ' +
|
||||||
|
'will investigate.'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default policy;
|
94
package-lock.json
generated
94
package-lock.json
generated
@ -2914,6 +2914,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
|
||||||
"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk="
|
"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk="
|
||||||
},
|
},
|
||||||
|
"clipboard": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz",
|
||||||
|
"integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"good-listener": "1.2.2",
|
||||||
|
"select": "1.1.2",
|
||||||
|
"tiny-emitter": "2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"clite": {
|
"clite": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/clite/-/clite-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/clite/-/clite-0.3.0.tgz",
|
||||||
@ -4028,6 +4039,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dashify/-/dashify-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/dashify/-/dashify-0.2.2.tgz",
|
||||||
"integrity": "sha1-agdBWgHJH69KMuONnfunH2HLIP4="
|
"integrity": "sha1-agdBWgHJH69KMuONnfunH2HLIP4="
|
||||||
},
|
},
|
||||||
|
"date-fns": {
|
||||||
|
"version": "1.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz",
|
||||||
|
"integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw=="
|
||||||
|
},
|
||||||
"date-now": {
|
"date-now": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
|
||||||
@ -4185,6 +4201,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||||
},
|
},
|
||||||
|
"delegate": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"denodeify": {
|
"denodeify": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz",
|
||||||
@ -5434,6 +5456,11 @@
|
|||||||
"strip-eof": "1.0.0"
|
"strip-eof": "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"exenv": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
|
||||||
|
"integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50="
|
||||||
|
},
|
||||||
"exit": {
|
"exit": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
||||||
@ -5575,6 +5602,13 @@
|
|||||||
"@types/express": "4.0.39",
|
"@types/express": "4.0.39",
|
||||||
"lodash": "4.17.4",
|
"lodash": "4.17.4",
|
||||||
"validator": "8.2.0"
|
"validator": "8.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"validator": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extend": {
|
"extend": {
|
||||||
@ -7488,6 +7522,15 @@
|
|||||||
"sparkles": "1.0.0"
|
"sparkles": "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"good-listener": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
|
||||||
|
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"delegate": "3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"google-auth-library": {
|
"google-auth-library": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz",
|
||||||
@ -13543,6 +13586,14 @@
|
|||||||
"plur": "1.0.0"
|
"plur": "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"prismjs": {
|
||||||
|
"version": "1.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.11.0.tgz",
|
||||||
|
"integrity": "sha1-KXrvM+t5Qhv9sZJzpQkspRWXDSk=",
|
||||||
|
"requires": {
|
||||||
|
"clipboard": "1.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"private": {
|
"private": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
|
||||||
@ -13980,6 +14031,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-helmet": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.0.tgz",
|
||||||
|
"integrity": "sha1-qBgR3yExOm1VxfBYxK66XW89l6c=",
|
||||||
|
"requires": {
|
||||||
|
"deep-equal": "1.0.1",
|
||||||
|
"object-assign": "4.1.1",
|
||||||
|
"prop-types": "15.6.0",
|
||||||
|
"react-side-effect": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-hot-api": {
|
"react-hot-api": {
|
||||||
"version": "0.4.7",
|
"version": "0.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/react-hot-api/-/react-hot-api-0.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-hot-api/-/react-hot-api-0.4.7.tgz",
|
||||||
@ -14089,6 +14151,15 @@
|
|||||||
"prop-types": "15.6.0"
|
"prop-types": "15.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-side-effect": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.1.3.tgz",
|
||||||
|
"integrity": "sha1-USwlq+DewXKDTEAB7FxR4E1BvFw=",
|
||||||
|
"requires": {
|
||||||
|
"exenv": "1.2.2",
|
||||||
|
"shallowequal": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-test-renderer": {
|
"react-test-renderer": {
|
||||||
"version": "15.6.2",
|
"version": "15.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-15.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-15.6.2.tgz",
|
||||||
@ -15001,6 +15072,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"select": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.4.1",
|
"version": "5.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
|
||||||
@ -15214,6 +15291,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"shallowequal": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-zlVXeVUKvo+HEv1e2KQF/csyeMKx2oHvatQ9l6XjCUj3agvC8XGf6R9HvIPDSmp8FNPvx7b5kaEJTRi7CqxtEw=="
|
||||||
|
},
|
||||||
"shebang-command": {
|
"shebang-command": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||||
@ -17066,6 +17148,12 @@
|
|||||||
"next-tick": "1.0.0"
|
"next-tick": "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tiny-emitter": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz",
|
||||||
@ -17844,9 +17932,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validator": {
|
"validator": {
|
||||||
"version": "8.2.0",
|
"version": "9.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/validator/-/validator-9.4.0.tgz",
|
||||||
"integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA=="
|
"integrity": "sha512-ftkCYp/7HrGdybVCuwSje07POAd93ksZJpb5GVDBzm8SLKIm3QMJcZugb5dOJsONBoWhIXl0jtoGHTyou3DAgA=="
|
||||||
},
|
},
|
||||||
"value-equal": {
|
"value-equal": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
"cors": "^2.8.4",
|
"cors": "^2.8.4",
|
||||||
"csurf": "^1.8.3",
|
"csurf": "^1.8.3",
|
||||||
"d3": "~3.5.17",
|
"d3": "~3.5.17",
|
||||||
|
"date-fns": "^1.29.0",
|
||||||
"debug": "^2.2.0",
|
"debug": "^2.2.0",
|
||||||
"dedent": "~0.7.0",
|
"dedent": "~0.7.0",
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
@ -105,6 +106,7 @@
|
|||||||
"passport-oauth": "^1.0.0",
|
"passport-oauth": "^1.0.0",
|
||||||
"passport-twitter": "^1.0.3",
|
"passport-twitter": "^1.0.3",
|
||||||
"pmx": "~0.6.2",
|
"pmx": "~0.6.2",
|
||||||
|
"prismjs": "^1.11.0",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"react": "^15.6.2",
|
"react": "^15.6.2",
|
||||||
"react-addons-css-transition-group": "~15.4.2",
|
"react-addons-css-transition-group": "~15.4.2",
|
||||||
@ -114,6 +116,7 @@
|
|||||||
"react-dom": "^15.6.2",
|
"react-dom": "^15.6.2",
|
||||||
"react-fontawesome": "^1.2.0",
|
"react-fontawesome": "^1.2.0",
|
||||||
"react-freecodecamp-search": "^1.4.1",
|
"react-freecodecamp-search": "^1.4.1",
|
||||||
|
"react-helmet": "^5.2.0",
|
||||||
"react-images": "^0.5.1",
|
"react-images": "^0.5.1",
|
||||||
"react-motion": "~0.4.2",
|
"react-motion": "~0.4.2",
|
||||||
"react-no-ssr": "^1.0.1",
|
"react-no-ssr": "^1.0.1",
|
||||||
@ -135,7 +138,7 @@
|
|||||||
"snyk": "^1.68.1",
|
"snyk": "^1.68.1",
|
||||||
"store": "git+https://github.com/berkeleytrue/store.js.git#feature/noop-server",
|
"store": "git+https://github.com/berkeleytrue/store.js.git#feature/noop-server",
|
||||||
"uuid": "^3.0.1",
|
"uuid": "^3.0.1",
|
||||||
"validator": "^8.2.0"
|
"validator": "^9.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"adler32": "~0.1.7",
|
"adler32": "~0.1.7",
|
||||||
|
74
public/css/loader.css
Normal file
74
public/css/loader.css
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
.loader {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-basis: 25%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes ball-scale-ripple-multiple {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: scale(0.1);
|
||||||
|
transform: scale(0.1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ball-scale-ripple-multiple {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: scale(0.1);
|
||||||
|
transform: scale(0.1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-scale-ripple-multiple {
|
||||||
|
position: relative;
|
||||||
|
-webkit-transform: translateY(-25px);
|
||||||
|
transform: translateY(-25px);
|
||||||
|
}
|
||||||
|
.ball-scale-ripple-multiple > div:nth-child(0) {
|
||||||
|
-webkit-animation-delay: -0.8s;
|
||||||
|
animation-delay: -0.8s;
|
||||||
|
}
|
||||||
|
.ball-scale-ripple-multiple > div:nth-child(1) {
|
||||||
|
-webkit-animation-delay: -0.6s;
|
||||||
|
animation-delay: -0.6s; }
|
||||||
|
.ball-scale-ripple-multiple > div:nth-child(2) {
|
||||||
|
-webkit-animation-delay: -0.4s;
|
||||||
|
animation-delay: -0.4s; }
|
||||||
|
.ball-scale-ripple-multiple > div:nth-child(3) {
|
||||||
|
-webkit-animation-delay: -0.2s;
|
||||||
|
animation-delay: -0.2s; }
|
||||||
|
.ball-scale-ripple-multiple > div {
|
||||||
|
-webkit-animation-fill-mode: both;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
position: absolute;
|
||||||
|
top: -42px;
|
||||||
|
left: -26px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 2px solid #006400;
|
||||||
|
-webkit-animation: ball-scale-ripple-multiple 1.25s 0s infinite cubic-bezier(0.21, 0.53, 0.56, 0.8);
|
||||||
|
animation: ball-scale-ripple-multiple 1.25s 0s infinite cubic-bezier(0.21, 0.53, 0.56, 0.8);
|
||||||
|
}
|
140
public/css/prism.css
Normal file
140
public/css/prism.css
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/* PrismJS 1.10.0
|
||||||
|
http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+jsx */
|
||||||
|
/**
|
||||||
|
* prism.js default theme for JavaScript, CSS and HTML
|
||||||
|
* Based on dabblet (http://dabblet.com)
|
||||||
|
* @author Lea Verou
|
||||||
|
*/
|
||||||
|
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: black;
|
||||||
|
background: none;
|
||||||
|
text-shadow: 0 1px white;
|
||||||
|
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
|
||||||
|
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: #b3d4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
|
||||||
|
code[class*="language-"]::selection, code[class*="language-"] ::selection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: #b3d4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: .5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre) > code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: #f5f2f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: .1em;
|
||||||
|
border-radius: .3em;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: slategray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.namespace {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
color: #905;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: #690;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string {
|
||||||
|
color: #a67f59;
|
||||||
|
background: hsla(0, 0%, 100%, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #07a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.function {
|
||||||
|
color: #DD4A68;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important,
|
||||||
|
.token.variable {
|
||||||
|
color: #e90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
@ -1,137 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Claim Your Responsive Web Design Certificate",
|
|
||||||
"order": 13,
|
|
||||||
"time": "5 minutes",
|
|
||||||
"challenges": [
|
|
||||||
{
|
|
||||||
"id": "587d78aa367417b2b2512aee",
|
|
||||||
"title": "Claim Your Responsive Web Design Certificate",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/GjTPLxI.jpg",
|
|
||||||
"An image of our Responsive Web Design Certificate",
|
|
||||||
"This challenge will give you your verified Responsive Web Design Certificate. Before we issue your certificate, we must verify that you have completed all of our basic and intermediate algorithm scripting challenges, and all our responsive web design projects. You must also accept our Academic Honesty Pledge. Click the button below to start this process.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/uLPsUko.jpg",
|
|
||||||
"The definition of plagiarism: Plagiarism (noun) - copying someone else’s work and presenting it as your own without crediting them",
|
|
||||||
"By clicking below, you pledge that all of your submitted code A) is code you or your pair personally wrote, or B) comes from open source libraries like jQuery, or C) has been clearly attributed to its original authors. You also give us permission to audit your challenge solutions and revoke your certificate if we discover evidence of plagiarism.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/cyRVnUa.jpg",
|
|
||||||
"An image of the text \"Responsive Web Design requirements\"",
|
|
||||||
"Let's confirm that you have completed all of our responsive web design projects. Click the button below to verify this.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/Q5Za9U6.jpg",
|
|
||||||
"An image of the word \"Congratulations\"",
|
|
||||||
"Congratulations! We've added your Responsive Web Design Certificate to your portfolio page. Unless you choose to hide your solutions, this certificate will remain publicly visible and verifiable.",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"challengeSeed": [
|
|
||||||
{
|
|
||||||
"properties": [
|
|
||||||
"isHonest",
|
|
||||||
"isRespWebDesignCert"
|
|
||||||
],
|
|
||||||
"apis": [
|
|
||||||
"/certificate/honest",
|
|
||||||
"/certificate/verify/responsive-web-design"
|
|
||||||
],
|
|
||||||
"stepIndex": [
|
|
||||||
1,
|
|
||||||
2
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tests": [
|
|
||||||
{
|
|
||||||
"id": "587d78af367417b2b2512b03",
|
|
||||||
"title": "Build a Survey Form"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7158d8c442eddfaeb5bd18",
|
|
||||||
"title": "Build a Tribute Page"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "587d78af367417b2b2512b04",
|
|
||||||
"title": "Build a Product Landing Page"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "587d78b0367417b2b2512b05",
|
|
||||||
"title": "Build a Technical Documentation Page"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7158d8c242eddfaeb5bd13",
|
|
||||||
"title": "Build a Personal Portfolio Webpage"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "Waypoint",
|
|
||||||
"challengeType": 7,
|
|
||||||
"translations": {
|
|
||||||
"es": {
|
|
||||||
"title": "Reclama tu certificado de Desarrollo de interfaces",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/GjTPLxI.jpg",
|
|
||||||
"An image of our Responsive Web Design Certificate",
|
|
||||||
"This challenge will give you your verified Responsive Web Design Certificate. Before we issue your certificate, we must verify that you have completed all of our responsive web design projects. You must also accept our Academic Honesty Pledge. Click the button below to start this process.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/HArFfMN.jpg",
|
|
||||||
"Plagio (nombre): acción y efecto de plagiar. Plagiar (verbo) - copiar en lo sustancial obras ajenas, dándolas como propias.",
|
|
||||||
"Al pulsar el botón siguiente, juras que todo el código en tus soluciones a los desafíos A) es código que tú o tu compañero escribieron personalmente, o B) proviene de librerías de código abierto como jQuery, o C) ha sido claramente atribuido a sus autores originales. También nos otorgas el permiso para auditar tus soluciones a los desafíos y revocar tu certificado si encontramos evidencia de plagio.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/cyRVnUa.jpg",
|
|
||||||
"An image of the text \"Responsive Web Design requirements\"",
|
|
||||||
"Let's confirm that you have completed all of our responsive web design projects. Click the button below to verify this.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/16SIhHO.jpg",
|
|
||||||
"Una imagen de la palabra \"Congratulations\"",
|
|
||||||
"¡Felicitaciones! Hemos agregado tu certificado de Desarrollo de interfaces a tu portafolio. A menos que elijas no mostrar tus soluciones, este certificado será públicamente visible y verificable.",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pt-br": {
|
|
||||||
"title": "Solicite seu Certificado de Design Responsivo",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/k8btNUB.jpg",
|
|
||||||
"Uma Imagem do Nosso Certificado de Design Responsivo",
|
|
||||||
"Este desafio lhe dará seu certificado verificado de Design Responsivo. Antes de emitir o seu certificado, devemos verificar se você completou todos os nossos desafios básicos e intermediários de algoritmos e todos os nossos projetos básicos, intermediários e avançados de desenvolvimento. Você também deve aceitar nosso compromisso de honestidade acadêmica. Clique no botão abaixo para iniciar este processo.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/uLPsUko.jpg",
|
|
||||||
"A definição de plágio: Plágio (substantivo) - copiar o trabalho de outra pessoa e apresentá-lo como seu sem creditar o autor",
|
|
||||||
"Ao clicar abaixo, você promete que todo o seu código enviado A) foi escrito por você ou o seu par pessoalmente, ou B) vem de bibliotecas de código aberto como o jQuery ou C) foi claramente atribuído a seus autores originais. Você também nos dá permissão para auditar suas soluções de desafios e revogar o seu certificado se descobrirmos evidências de plágio.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/UedoV2G.jpg",
|
|
||||||
"Uma imagem do texto \"Requisitos do Certificado de Desenvolvimento Front-End\"",
|
|
||||||
"Vamos confirmar que você completou todos os nossos desafios de algoritmos básicos e intermediários e todos os nossos projetos básicos, intermediários e avançados de desenvolvimento. Clique no botão abaixo para verificar.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/Q5Za9U6.jpg",
|
|
||||||
"Uma imagem da palavra \"Parabéns\"",
|
|
||||||
"Parabéns! Adicionamos o seu Certificado de Design Responsivo à sua página de portfólio. A menos que você opte por esconder suas soluções, este certificado permanecerá publicamente visível e verificável",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Claim Your Front End Libraries Certificate",
|
|
||||||
"order": 13,
|
|
||||||
"time": "5 minutes",
|
|
||||||
"challenges": [
|
|
||||||
{
|
|
||||||
"id": "587d7dbb367417b2b2512bad",
|
|
||||||
"title": "Claim Your Front End Libraries Certificate",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/vOtZumH.jpg",
|
|
||||||
"An image of our Front End Libraries Certificate",
|
|
||||||
"This challenge will give you your verified Front End Libraries Certificate. Before we issue your certificate, we must verify that you have completed all of our front end libraries projects. You must also accept our Academic Honesty Pledge. Click the button below to start this process.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/uLPsUko.jpg",
|
|
||||||
"The definition of plagiarism: Plagiarism (noun) - copying someone else’s work and presenting it as your own without crediting them",
|
|
||||||
"By clicking below, you pledge that all of your submitted code A) is code you or your pair personally wrote, or B) comes from open source libraries like jQuery, or C) has been clearly attributed to its original authors. You also give us permission to audit your challenge solutions and revoke your certificate if we discover evidence of plagiarism.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/GJeTCMS.jpg",
|
|
||||||
"An image of the text \"Front End Libraries Certificate requirements\"",
|
|
||||||
"Let's confirm that you have completed all of our front end libraries projects. Click the button below to verify this.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/Q5Za9U6.jpg",
|
|
||||||
"An image of the word \"Congratulations\"",
|
|
||||||
"Congratulations! We've added your Front End Libraries Certificate to your portfolio page. Unless you choose to hide your solutions, this certificate will remain publicly visible and verifiable.",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"challengeSeed": [
|
|
||||||
{
|
|
||||||
"properties": [
|
|
||||||
"isHonest",
|
|
||||||
"isFrontEndLibsCert"
|
|
||||||
],
|
|
||||||
"apis": [
|
|
||||||
"/certificate/honest",
|
|
||||||
"/certificate/verify/front-end-libraries"
|
|
||||||
],
|
|
||||||
"stepIndex": [
|
|
||||||
1,
|
|
||||||
2
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tests": [
|
|
||||||
{
|
|
||||||
"id": "bd7158d8c442eddfaeb5bd0f",
|
|
||||||
"title": "Build a Pomodoro Clock"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7158d8c442eddfaeb5bd17",
|
|
||||||
"title": "Build a JavaScript Calculator"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "587d7dbc367417b2b2512bae",
|
|
||||||
"title": "Build a Drum Machine"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7157d8c242eddfaeb5bd13",
|
|
||||||
"title": "Build a Markdown Previewer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7158d8c442eddfaeb5bd13",
|
|
||||||
"title": "Build a Random Quote Machine"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "Waypoint",
|
|
||||||
"challengeType": 7,
|
|
||||||
"translations": {
|
|
||||||
"es": {
|
|
||||||
"title": "Reclama tu certificado de Desarrollo de interfaces",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/k8btNUB.jpg",
|
|
||||||
"Una imagen que muestra nuestro certificado de Desarrollo de interfaces",
|
|
||||||
"Este desafío te otorga tu certificado autenticado de Desarrollo de interfaces. Antes de que podamos emitir tu certificado, debemos verificar que has completado todos los desafíos básicos e intermedios de diseño de algoritmos, y todos los proyectos básicos e intermedios de desarrollo de interfaces. También debes aceptar nuestro Juramento de honestidad académica. Pulsa el botón siguiente para iniciar este proceso.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/HArFfMN.jpg",
|
|
||||||
"Plagio (nombre): acción y efecto de plagiar. Plagiar (verbo) - copiar en lo sustancial obras ajenas, dándolas como propias.",
|
|
||||||
"Al pulsar el botón siguiente, juras que todo el código en tus soluciones a los desafíos A) es código que tú o tu compañero escribieron personalmente, o B) proviene de librerías de código abierto como jQuery, o C) ha sido claramente atribuido a sus autores originales. También nos otorgas el permiso para auditar tus soluciones a los desafíos y revocar tu certificado si encontramos evidencia de plagio.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/14F2Van.jpg",
|
|
||||||
"Una imagen del texto \"Front End Development Certificate requirements\"",
|
|
||||||
"Confirmemos que has completado todos nuestros desafíos básicos e intermedios de diseño de algoritmos, y todos nuestros proyectos básicos e intermedios de desarrollo de interfaces. Pulsa el botón siguiente para hacer la verificación.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/16SIhHO.jpg",
|
|
||||||
"Una imagen de la palabra \"Congratulations\"",
|
|
||||||
"¡Felicitaciones! Hemos agregado tu certificado de Desarrollo de interfaces a tu portafolio. A menos que elijas no mostrar tus soluciones, este certificado será públicamente visible y verificable.",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pt-br": {
|
|
||||||
"title": "Solicite seu Certificado de Bibliotecas Front End",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/vOtZumH.jpg",
|
|
||||||
"Uma imagem do nosso Certificado de Bibliotecas Front End",
|
|
||||||
"This challenge will give you your verified Front End Libraries Certificate. Before we issue your certificate, we must verify that you have completed all of our front end libraries projects. You must also accept our Academic Honesty Pledge. Click the button below to start this process.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/uLPsUko.jpg",
|
|
||||||
"A definição de plágio: Plágio (substantivo) - copiar o trabalho de outra pessoa e apresentá-lo como seu sem creditar o autor",
|
|
||||||
"Ao clicar abaixo, você declara que todo o seu código enviado A) foi escrito por você ou pelo seu par pessoalmente, ou B) vem de bibliotecas de código aberto como o jQuery ou C) foi claramente atribuído a seus autores originais. Você também nos dá permissão para auditar suas soluções de desafios e revogar o seu certificado se descobrirmos evidências de plágio.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/UedoV2G.jpg",
|
|
||||||
"Uma imagem do texto \"Requisitos do Certificado de Desenvolvimento Front-End\"",
|
|
||||||
"Vamos confirmar que você completou todos os nossos desafios de algoritmos básicos e intermediários e todos os nossos projetos básicos, intermediários e avançados de desenvolvimento. Clique no botão abaixo para verificar.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/Q5Za9U6.jpg",
|
|
||||||
"Uma imagem da palavra \"Parabéns\"",
|
|
||||||
"Parabéns! Adicionamos o seu Certificado de Bibliotecas Front End à sua página de portfólio. A menos que você opte por esconder suas soluções, este certificado permanecerá publicamente visível e verificável.",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Claim Your Data Visualization Certificate",
|
|
||||||
"order": 13,
|
|
||||||
"time": "5 minutes",
|
|
||||||
"challenges": [
|
|
||||||
{
|
|
||||||
"id": "587d7fa5367417b2b2512bbe",
|
|
||||||
"title": "Claim Your Data Visualization Certificate",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/N8drT4I.jpg",
|
|
||||||
"An image of our Data Visualization Certificate",
|
|
||||||
"This challenge will give you your verified Data Visualization Certificate. Before we issue your certificate, we must verify that you have completed all of our data visualisation projects. You must also accept our Academic Honesty Pledge. Click the button below to start this process.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/uLPsUko.jpg",
|
|
||||||
"The definition of plagiarism: Plagiarism (noun) - copying someone else’s work and presenting it as your own without crediting them",
|
|
||||||
"By clicking below, you pledge that all of your submitted code A) is code you or your pair personally wrote, or B) comes from open source libraries like jQuery, or C) has been clearly attributed to its original authors. You also give us permission to audit your challenge solutions and revoke your certificate if we discover evidence of plagiarism.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/BUaEvDo.jpg",
|
|
||||||
"An image of the text \"Data Visualization Certificate requirements\"",
|
|
||||||
"Let's confirm that you have completed data visualisation projects. Click the button below to verify this.", "#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/Q5Za9U6.jpg",
|
|
||||||
"An image of the word \"Congratulations\"",
|
|
||||||
"Congratulations! We've added your Data Visualization Certificate to your portfolio page. Unless you choose to hide your solutions, this certificate will remain publicly visible and verifiable.",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"challengeSeed": [
|
|
||||||
{
|
|
||||||
"properties": [
|
|
||||||
"isHonest",
|
|
||||||
"isDataVisCert"
|
|
||||||
],
|
|
||||||
"apis": [
|
|
||||||
"/certificate/honest",
|
|
||||||
"/certificate/verify/data-visualization"
|
|
||||||
],
|
|
||||||
"stepIndex": [
|
|
||||||
1,
|
|
||||||
2
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tests": [
|
|
||||||
{
|
|
||||||
"id": "587d7fa6367417b2b2512bc0",
|
|
||||||
"title": "Visualize Data with a Treemap Diagram"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "587d7fa6367417b2b2512bbf",
|
|
||||||
"title": "Visualize Data with a Choropleth Map"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7188d8c242eddfaeb5bd13",
|
|
||||||
"title": "Visualize Data with a Heat Map"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7178d8c242eddfaeb5bd13",
|
|
||||||
"title": "Visualize Data with a Scatterplot Graph"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7168d8c242eddfaeb5bd13",
|
|
||||||
"title": "Visualize Data with a Bar Chart"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "Waypoint",
|
|
||||||
"challengeType": 7,
|
|
||||||
"translations": {
|
|
||||||
"es": {
|
|
||||||
"title": "Reclama tu certificado de Desarrollo de interfaces",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/N8drT4I.jpg",
|
|
||||||
"Una imagen que muestra nuestro certificado de Desarrollo de interfaces",
|
|
||||||
"This challenge will give you your verified Data Visualization Certificate. Before we issue your certificate, we must verify that you have completed all of our data visualisation projects. You must also accept our Academic Honesty Pledge. Click the button below to start this process.", ""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/HArFfMN.jpg",
|
|
||||||
"Plagio (nombre): acción y efecto de plagiar. Plagiar (verbo) - copiar en lo sustancial obras ajenas, dándolas como propias.",
|
|
||||||
"Al pulsar el botón siguiente, juras que todo el código en tus soluciones a los desafíos A) es código que tú o tu compañero escribieron personalmente, o B) proviene de librerías de código abierto como jQuery, o C) ha sido claramente atribuido a sus autores originales. También nos otorgas el permiso para auditar tus soluciones a los desafíos y revocar tu certificado si encontramos evidencia de plagio.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/BUaEvDo.jpg",
|
|
||||||
"An image of the text \"Data Visualization Certificate requirements\"",
|
|
||||||
"Let's confirm that you have completed data visualisation projects. Click the button below to verify this.", "Confirmemos que has completado todos nuestros desafíos básicos e intermedios de diseño de algoritmos, y todos nuestros proyectos básicos e intermedios de desarrollo de interfaces. Pulsa el botón siguiente para hacer la verificación.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/16SIhHO.jpg",
|
|
||||||
"Una imagen de la palabra \"Congratulations\"",
|
|
||||||
"¡Felicitaciones! Hemos agregado tu certificado de Desarrollo de interfaces a tu portafolio. A menos que elijas no mostrar tus soluciones, este certificado será públicamente visible y verificable.",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Claim Your APIs and Microservices Certificate",
|
|
||||||
"order": 13,
|
|
||||||
"time": "5 minutes",
|
|
||||||
"challenges": [
|
|
||||||
{
|
|
||||||
"id": "587d7fb3367417b2b2512bf9",
|
|
||||||
"title": "Claim Your APIs and Microservices Certificate",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/gfH7j5B.jpg",
|
|
||||||
"An image of our APIs and Microservices Certificate",
|
|
||||||
"This challenge will give you your verified APIs and Microservices Certificate. Before we issue your certificate, we must verify that you have completed all of our apis and microservices projects. You must also accept our Academic Honesty Pledge. Click the button below to start this process.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/uLPsUko.jpg",
|
|
||||||
"The definition of plagiarism: Plagiarism (noun) - copying someone else’s work and presenting it as your own without crediting them",
|
|
||||||
"By clicking below, you pledge that all of your submitted code A) is code you or your pair personally wrote, or B) comes from open source libraries like jQuery, or C) has been clearly attributed to its original authors. You also give us permission to audit your challenge solutions and revoke your certificate if we discover evidence of plagiarism.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/IBTfUzO.jpg",
|
|
||||||
"An image of the text \"APIs and Microservices Certificate requirements\"",
|
|
||||||
"Let's confirm that you have completed all of our apis and microservices projects. Click the button below to verify this.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/Q5Za9U6.jpg",
|
|
||||||
"An image of the word \"Congratulations\"",
|
|
||||||
"Congratulations! We've added your APIs and Microservices Certificate to your portfolio page. Unless you choose to hide your solutions, this certificate will remain publicly visible and verifiable.",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"challengeSeed": [
|
|
||||||
{
|
|
||||||
"properties": [
|
|
||||||
"isHonest",
|
|
||||||
"isApisMicroservicesCert"
|
|
||||||
],
|
|
||||||
"apis": [
|
|
||||||
"/certificate/honest",
|
|
||||||
"/certificate/verify/apis-microservices"
|
|
||||||
],
|
|
||||||
"stepIndex": [
|
|
||||||
1,
|
|
||||||
2
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tests": [
|
|
||||||
{
|
|
||||||
"id": "bd7158d8c443edefaeb5bdef",
|
|
||||||
"title": "Timestamp Microservice"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7158d8c443edefaeb5bdff",
|
|
||||||
"title": "Request Header Parser Microservice"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7158d8c443edefaeb5bd0e",
|
|
||||||
"title": "URL Shortener Microservice"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7158d8c443edefaeb5bd0f",
|
|
||||||
"title": "File Metadata Microservice"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bd7158d8c443edefaeb5bdee",
|
|
||||||
"title": "Exercise Tracker"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "Waypoint",
|
|
||||||
"challengeType": 7,
|
|
||||||
"translations": {
|
|
||||||
"es": {
|
|
||||||
"title": "Reclama tu certificado de Desarrollo de interfaces",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/gfH7j5B.jpg",
|
|
||||||
"Una imagen que muestra nuestro certificado de Desarrollo de interfaces",
|
|
||||||
"This challenge will give you your verified APIs and Microservices Certificate. Before we issue your certificate, we must verify that you have completed all of our apis and microservices projects. You must also accept our Academic Honesty Pledge. Click the button below to start this process.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/HArFfMN.jpg",
|
|
||||||
"Plagio (nombre): acción y efecto de plagiar. Plagiar (verbo) - copiar en lo sustancial obras ajenas, dándolas como propias.",
|
|
||||||
"Al pulsar el botón siguiente, juras que todo el código en tus soluciones a los desafíos A) es código que tú o tu compañero escribieron personalmente, o B) proviene de librerías de código abierto como jQuery, o C) ha sido claramente atribuido a sus autores originales. También nos otorgas el permiso para auditar tus soluciones a los desafíos y revocar tu certificado si encontramos evidencia de plagio.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/IBTfUzO.jpg",
|
|
||||||
"An image of the text \"APIs and Microservices Certificate requirements\"",
|
|
||||||
"Let's confirm that you have completed all of our apis and microservices projects. Click the button below to verify this.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/16SIhHO.jpg",
|
|
||||||
"Una imagen de la palabra \"Congratulations\"",
|
|
||||||
"¡Felicitaciones! Hemos agregado tu certificado de Desarrollo de interfaces a tu portafolio. A menos que elijas no mostrar tus soluciones, este certificado será públicamente visible y verificable.",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Claim Your Information Security and Quality Assurance Certificate",
|
|
||||||
"order": 13,
|
|
||||||
"time": "5 minutes",
|
|
||||||
"challenges": [
|
|
||||||
{
|
|
||||||
"id": "587d8247367417b2b2512c35",
|
|
||||||
"title": "Claim Your Information Security and Quality Assurance Certificate",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/YhKzGLb.jpg",
|
|
||||||
"An image of our Information Security and Quality Assurance Certificate",
|
|
||||||
"This challenge will give you your verified Information Security and Quality Assurance Certificate. Before we issue your certificate, we must verify that you have completed all of our information security and quality assurance projects. You must also accept our Academic Honesty Pledge. Click the button below to start this process.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/uLPsUko.jpg",
|
|
||||||
"The definition of plagiarism: Plagiarism (noun) - copying someone else’s work and presenting it as your own without crediting them",
|
|
||||||
"By clicking below, you pledge that all of your submitted code A) is code you or your pair personally wrote, or B) comes from open source libraries like jQuery, or C) has been clearly attributed to its original authors. You also give us permission to audit your challenge solutions and revoke your certificate if we discover evidence of plagiarism.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/TM4KGfb.jpg",
|
|
||||||
"An image of the text \"Information Security and Quality Assurance Certificate requirements\"",
|
|
||||||
"Let's confirm that you have completed all of our information security and quality assurance projects. Click the button below to verify this.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/Q5Za9U6.jpg",
|
|
||||||
"An image of the word \"Congratulations\"",
|
|
||||||
"Congratulations! We've added your Information Security and Quality Assurance Certificate to your portfolio page. Unless you choose to hide your solutions, this certificate will remain publicly visible and verifiable.",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"challengeSeed": [
|
|
||||||
{
|
|
||||||
"properties": [
|
|
||||||
"isHonest",
|
|
||||||
"isInfosecQaCert"
|
|
||||||
],
|
|
||||||
"apis": [
|
|
||||||
"/certificate/honest",
|
|
||||||
"/certificate/verify/information-security-quality-assurance"
|
|
||||||
],
|
|
||||||
"stepIndex": [
|
|
||||||
1,
|
|
||||||
2
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tests": [
|
|
||||||
{
|
|
||||||
"id": "587d8249367417b2b2512c42",
|
|
||||||
"title": "Issue Tracker"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "587d8249367417b2b2512c41",
|
|
||||||
"title": "Metric-Imperial Converter"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "587d824a367417b2b2512c43",
|
|
||||||
"title": "Personal Library"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "587d824a367417b2b2512c44",
|
|
||||||
"title": "Stock Price Checker"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "587d824a367417b2b2512c45",
|
|
||||||
"title": "Anonymous Message Board"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "Waypoint",
|
|
||||||
"challengeType": 7,
|
|
||||||
"translations": {
|
|
||||||
"es": {
|
|
||||||
"title": "Reclama tu certificado de Desarrollo de interfaces",
|
|
||||||
"description": [
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/YhKzGLb.jpg",
|
|
||||||
"Una imagen que muestra nuestro certificado de Desarrollo de interfaces",
|
|
||||||
"This challenge will give you your verified Information Security and Quality Assurance Certificate. Before we issue your certificate, we must verify that you have completed all of our information security and quality assurance projects. You must also accept our Academic Honesty Pledge. Click the button below to start this process.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/HArFfMN.jpg",
|
|
||||||
"Plagio (nombre): acción y efecto de plagiar. Plagiar (verbo) - copiar en lo sustancial obras ajenas, dándolas como propias.",
|
|
||||||
"Al pulsar el botón siguiente, juras que todo el código en tus soluciones a los desafíos A) es código que tú o tu compañero escribieron personalmente, o B) proviene de librerías de código abierto como jQuery, o C) ha sido claramente atribuido a sus autores originales. También nos otorgas el permiso para auditar tus soluciones a los desafíos y revocar tu certificado si encontramos evidencia de plagio.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/TM4KGfb.jpg",
|
|
||||||
"An image of the text \"Information Security and Quality Assurance Certificate requirements\"",
|
|
||||||
"Let's confirm that you have completed all of our information security and quality assurance projects. Click the button below to verify this.",
|
|
||||||
"#"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"https://i.imgur.com/16SIhHO.jpg",
|
|
||||||
"Una imagen de la palabra \"Congratulations\"",
|
|
||||||
"¡Felicitaciones! Hemos agregado tu certificado de Desarrollo de interfaces a tu portafolio. A menos que elijas no mostrar tus soluciones, este certificado será públicamente visible y verificable.",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "API's and Microservices Certificate",
|
||||||
|
"order": 5,
|
||||||
|
"isPrivate": true,
|
||||||
|
"challenges": [
|
||||||
|
{
|
||||||
|
"id": "561add10cb82ac38a17523bc",
|
||||||
|
"title": "API's and Microservices Certificate",
|
||||||
|
"challengeType": 7,
|
||||||
|
"description": [],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"isPrivate": true,
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"id": "bd7158d8c443edefaeb5bdef",
|
||||||
|
"title": "Timestamp Microservice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bd7158d8c443edefaeb5bdff",
|
||||||
|
"title": "Request Header Parser Microservice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bd7158d8c443edefaeb5bd0e",
|
||||||
|
"title": "URL Shortener Microservice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bd7158d8c443edefaeb5bdee",
|
||||||
|
"title": "Exercise Tracker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bd7158d8c443edefaeb5bd0f",
|
||||||
|
"title": "File Metadata Microservice"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "Data Visualization Certificate",
|
||||||
|
"order": 4,
|
||||||
|
"isPrivate": true,
|
||||||
|
"challenges": [
|
||||||
|
{
|
||||||
|
"id": "5a553ca864b52e1d8bceea14",
|
||||||
|
"title": "Data Visualization Certificate",
|
||||||
|
"challengeType": 7,
|
||||||
|
"description": [],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"isPrivate": true,
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"id": "bd7168d8c242eddfaeb5bd13",
|
||||||
|
"title": "Visualize Data with a Bar Chart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bd7178d8c242eddfaeb5bd13",
|
||||||
|
"title": "Visualize Data with a Scatterplot Graph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bd7188d8c242eddfaeb5bd13",
|
||||||
|
"title": "Visualize Data with a Heat Map"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d7fa6367417b2b2512bbf",
|
||||||
|
"title": "Visualize Data with a Choropleth Map"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d7fa6367417b2b2512bc0",
|
||||||
|
"title": "Visualize Data with a Treemap Diagram"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "Front End Libraries Certificate",
|
||||||
|
"order": 3,
|
||||||
|
"isPrivate": true,
|
||||||
|
"challenges": [
|
||||||
|
{
|
||||||
|
"id": "561acd10cb82ac38a17513bc",
|
||||||
|
"title": "Front End Libraries Certificate",
|
||||||
|
"challengeType": 7,
|
||||||
|
"description": [],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"isPrivate": true,
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"id": "bd7158d8c442eddfaeb5bd13",
|
||||||
|
"title": "Build a Random Quote Machine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bd7157d8c242eddfaeb5bd13",
|
||||||
|
"title": "Build a Markdown Previewer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d7dbc367417b2b2512bae",
|
||||||
|
"title": "Build a Drum Machine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bd7158d8c442eddfaeb5bd17",
|
||||||
|
"title": "Build a JavaScript Calculator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bd7158d8c442eddfaeb5bd0f",
|
||||||
|
"title": "Build a Pomodoro Clock"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "Information, Securtiy and Quality Assurance Certificate",
|
||||||
|
"order": 6,
|
||||||
|
"isPrivate": true,
|
||||||
|
"challenges": [
|
||||||
|
{
|
||||||
|
"id": "561add10cb82ac38a17213bc",
|
||||||
|
"title": "Information, Securtiy and Quality Assurance Certificate",
|
||||||
|
"challengeType": 7,
|
||||||
|
"description": [],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"isPrivate": true,
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"id": "587d8249367417b2b2512c42",
|
||||||
|
"title": "Issue Tracker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8249367417b2b2512c41",
|
||||||
|
"title": "Metric-Imperial Converter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824a367417b2b2512c43",
|
||||||
|
"title": "Personal Library"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824a367417b2b2512c44",
|
||||||
|
"title": "Stock Price Checker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824a367417b2b2512c45",
|
||||||
|
"title": "Anonymous Message Board"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "JavaScript Algorithms and Data Structures Certificate",
|
||||||
|
"order": 2,
|
||||||
|
"isPrivate": true,
|
||||||
|
"challenges": [
|
||||||
|
{
|
||||||
|
"id": "561abd10cb81ac38a17513bc",
|
||||||
|
"title": "JavaScript Algorithms and Data Structures Certificate",
|
||||||
|
"challengeType": 7,
|
||||||
|
"description": [],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"isPrivate": true,
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"id": "aaa48de84e1ecc7c742e1124",
|
||||||
|
"title": "Palindrome Checker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a7f4d8f2483413a6ce226cac",
|
||||||
|
"title": "Roman Numeral Converter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "56533eb9ac21ba0edf2244e2",
|
||||||
|
"title": "Caesars Cipher"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "aff0395860f5d3034dc0bfc9",
|
||||||
|
"title": "Telephone Number Validator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "aa2e6f85cab2ab736c9a9b24",
|
||||||
|
"title": "Cash Register"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "Responsive Web Design Certificate",
|
||||||
|
"order": 1,
|
||||||
|
"isPrivate": true,
|
||||||
|
"challenges": [
|
||||||
|
{
|
||||||
|
"id": "561add10cb82ac38a17513bc",
|
||||||
|
"title": "Responsive Web Design Certificate",
|
||||||
|
"challengeType": 7,
|
||||||
|
"description": [],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"isPrivate": true,
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"id": "bd7158d8c442eddfaeb5bd18",
|
||||||
|
"title": "Build a Tribute Page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d78af367417b2b2512b03",
|
||||||
|
"title": "Build a Survey Form"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d78af367417b2b2512b04",
|
||||||
|
"title": "Build a Product Landing Page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d78b0367417b2b2512b05",
|
||||||
|
"title": "Build a Technical Documentation Page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d78b0367417b2b2512b06",
|
||||||
|
"title": "Build a Personal Portfolio Webpage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -8,8 +8,8 @@ const _ = require('lodash');
|
|||||||
const utils = require('../server/utils');
|
const utils = require('../server/utils');
|
||||||
const getChallenges = require('./getChallenges');
|
const getChallenges = require('./getChallenges');
|
||||||
const app = require('../server/server');
|
const app = require('../server/server');
|
||||||
|
|
||||||
const createDebugger = require('debug');
|
const createDebugger = require('debug');
|
||||||
|
|
||||||
const log = createDebugger('fcc:seed');
|
const log = createDebugger('fcc:seed');
|
||||||
// force logger to always output
|
// force logger to always output
|
||||||
// this may be brittle
|
// this may be brittle
|
||||||
@ -51,6 +51,7 @@ Observable.combineLatest(
|
|||||||
const message = challengeSpec.message;
|
const message = challengeSpec.message;
|
||||||
const required = challengeSpec.required || [];
|
const required = challengeSpec.required || [];
|
||||||
const template = challengeSpec.template;
|
const template = challengeSpec.template;
|
||||||
|
const isPrivate = !!challengeSpec.isPrivate;
|
||||||
|
|
||||||
log('parsed %s successfully', blockName);
|
log('parsed %s successfully', blockName);
|
||||||
|
|
||||||
@ -63,12 +64,13 @@ Observable.combineLatest(
|
|||||||
title: blockName,
|
title: blockName,
|
||||||
name: nameify(blockName),
|
name: nameify(blockName),
|
||||||
dashedName: dasherize(blockName),
|
dashedName: dasherize(blockName),
|
||||||
superOrder: superOrder,
|
superOrder,
|
||||||
superBlock: superBlock,
|
superBlock,
|
||||||
superBlockMessage: message,
|
superBlockMessage: message,
|
||||||
order: order,
|
order,
|
||||||
time: time,
|
time,
|
||||||
isLocked: isLocked
|
isLocked,
|
||||||
|
isPrivate
|
||||||
};
|
};
|
||||||
|
|
||||||
return createBlocks(block)
|
return createBlocks(block)
|
||||||
@ -110,6 +112,7 @@ Observable.combineLatest(
|
|||||||
challenge.isBeta = challenge.isBeta || isBeta;
|
challenge.isBeta = challenge.isBeta || isBeta;
|
||||||
challenge.isComingSoon = challenge.isComingSoon || isComingSoon;
|
challenge.isComingSoon = challenge.isComingSoon || isComingSoon;
|
||||||
challenge.isLocked = challenge.isLocked || isLocked;
|
challenge.isLocked = challenge.isLocked || isLocked;
|
||||||
|
challenge.isPrivate = challenge.isPrivate || isPrivate;
|
||||||
challenge.time = challengeSpec.time;
|
challenge.time = challengeSpec.time;
|
||||||
challenge.superOrder = superOrder;
|
challenge.superOrder = superOrder;
|
||||||
challenge.superBlock = superBlock
|
challenge.superBlock = superBlock
|
||||||
|
@ -7,9 +7,11 @@ import { check } from 'express-validator/check';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ifUserRedirectTo,
|
ifUserRedirectTo,
|
||||||
|
ifNoUserRedirectTo,
|
||||||
createValidatorErrorHandler
|
createValidatorErrorHandler
|
||||||
} from '../utils/middleware';
|
} from '../utils/middleware';
|
||||||
import { wrapHandledError } from '../utils/create-handled-error.js';
|
import { wrapHandledError } from '../utils/create-handled-error.js';
|
||||||
|
import { homeURL } from '../../common/utils/constantStrings.json';
|
||||||
|
|
||||||
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
|
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
|
||||||
// const debug = debugFactory('fcc:boot:auth');
|
// const debug = debugFactory('fcc:boot:auth');
|
||||||
@ -22,6 +24,7 @@ module.exports = function enableAuthentication(app) {
|
|||||||
// loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html
|
// loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html
|
||||||
app.enableAuth();
|
app.enableAuth();
|
||||||
const ifUserRedirect = ifUserRedirectTo();
|
const ifUserRedirect = ifUserRedirectTo();
|
||||||
|
const ifNoUserRedirectHome = ifNoUserRedirectTo(homeURL);
|
||||||
const router = app.loopback.Router();
|
const router = app.loopback.Router();
|
||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
const { AuthToken, User } = app.models;
|
const { AuthToken, User } = app.models;
|
||||||
@ -79,7 +82,8 @@ module.exports = function enableAuthentication(app) {
|
|||||||
const {
|
const {
|
||||||
query: {
|
query: {
|
||||||
email: encodedEmail,
|
email: encodedEmail,
|
||||||
token: authTokenId
|
token: authTokenId,
|
||||||
|
emailChange
|
||||||
} = {}
|
} = {}
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
@ -122,14 +126,16 @@ module.exports = function enableAuthentication(app) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (user.email !== email) {
|
if (user.email !== email) {
|
||||||
throw wrapHandledError(
|
if (!emailChange || (emailChange && user.newEmail !== email)) {
|
||||||
new Error('user email does not match'),
|
throw wrapHandledError(
|
||||||
{
|
new Error('user email does not match'),
|
||||||
type: 'info',
|
{
|
||||||
message: defaultErrorMsg,
|
type: 'info',
|
||||||
redirectTo: '/email-signin'
|
message: defaultErrorMsg,
|
||||||
}
|
redirectTo: '/email-signin'
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return authToken.validate$()
|
return authToken.validate$()
|
||||||
.map(isValid => {
|
.map(isValid => {
|
||||||
@ -185,6 +191,13 @@ module.exports = function enableAuthentication(app) {
|
|||||||
getPasswordlessAuth
|
getPasswordlessAuth
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/passwordless-change',
|
||||||
|
ifNoUserRedirectHome,
|
||||||
|
passwordlessGetValidators,
|
||||||
|
getPasswordlessAuth
|
||||||
|
);
|
||||||
|
|
||||||
const passwordlessPostValidators = [
|
const passwordlessPostValidators = [
|
||||||
check('email')
|
check('email')
|
||||||
.isEmail()
|
.isEmail()
|
||||||
|
@ -7,28 +7,31 @@ import debug from 'debug';
|
|||||||
import { isEmail } from 'validator';
|
import { isEmail } from 'validator';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ifNoUser401,
|
ifNoUser401
|
||||||
ifNoUserSend
|
|
||||||
} from '../utils/middleware';
|
} from '../utils/middleware';
|
||||||
|
|
||||||
import { observeQuery } from '../utils/rx';
|
import { observeQuery } from '../utils/rx';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
// legacy
|
||||||
|
frontEndChallengeId,
|
||||||
|
backEndChallengeId,
|
||||||
|
dataVisId,
|
||||||
|
|
||||||
|
// modern
|
||||||
respWebDesignId,
|
respWebDesignId,
|
||||||
frontEndLibsId,
|
frontEndLibsId,
|
||||||
|
dataVis2018Id,
|
||||||
jsAlgoDataStructId,
|
jsAlgoDataStructId,
|
||||||
frontEndChallengeId,
|
|
||||||
dataVisId,
|
|
||||||
apisMicroservicesId,
|
apisMicroservicesId,
|
||||||
backEndChallengeId,
|
|
||||||
infosecQaId
|
infosecQaId
|
||||||
} from '../utils/constantStrings.json';
|
} from '../utils/constantStrings.json';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
completeCommitment$
|
completeCommitment$
|
||||||
} from '../utils/commit';
|
} from '../utils/commit';
|
||||||
|
|
||||||
import certTypes from '../utils/certTypes.json';
|
import certTypes from '../utils/certTypes.json';
|
||||||
|
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
|
||||||
|
|
||||||
const log = debug('fcc:certification');
|
const log = debug('fcc:certification');
|
||||||
const renderCertifedEmail = loopback.template(path.join(
|
const renderCertifedEmail = loopback.template(path.join(
|
||||||
@ -38,12 +41,9 @@ const renderCertifedEmail = loopback.template(path.join(
|
|||||||
'emails',
|
'emails',
|
||||||
'certified.ejs'
|
'certified.ejs'
|
||||||
));
|
));
|
||||||
const sendMessageToNonUser = ifNoUserSend(
|
|
||||||
'must be logged in to complete.'
|
|
||||||
);
|
|
||||||
|
|
||||||
function isCertified(ids, challengeMap = {}) {
|
function isCertified(ids, challengeMap = {}) {
|
||||||
return _.every(ids, ({ id }) => challengeMap[id]);
|
return _.every(ids, ({ id }) => _.has(challengeMap, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIdsForCert$(id, Challenge) {
|
function getIdsForCert$(id, Challenge) {
|
||||||
@ -120,12 +120,16 @@ export default function certificate(app) {
|
|||||||
const { Email, Challenge } = app.models;
|
const { Email, Challenge } = app.models;
|
||||||
|
|
||||||
const certTypeIds = {
|
const certTypeIds = {
|
||||||
|
// legacy
|
||||||
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge),
|
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge),
|
||||||
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge),
|
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge),
|
||||||
|
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
|
||||||
|
|
||||||
|
// modern
|
||||||
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
|
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
|
||||||
[certTypes.frontEndLibs]: getIdsForCert$(frontEndLibsId, Challenge),
|
[certTypes.frontEndLibs]: getIdsForCert$(frontEndLibsId, Challenge),
|
||||||
|
[certTypes.dataVis2018]: getIdsForCert$(dataVis2018Id, Challenge),
|
||||||
[certTypes.jsAlgoDataStruct]: getIdsForCert$(jsAlgoDataStructId, Challenge),
|
[certTypes.jsAlgoDataStruct]: getIdsForCert$(jsAlgoDataStructId, Challenge),
|
||||||
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
|
|
||||||
[certTypes.apisMicroservices]: getIdsForCert$(
|
[certTypes.apisMicroservices]: getIdsForCert$(
|
||||||
apisMicroservicesId,
|
apisMicroservicesId,
|
||||||
Challenge
|
Challenge
|
||||||
@ -133,78 +137,65 @@ export default function certificate(app) {
|
|||||||
[certTypes.infosecQa]: getIdsForCert$(infosecQaId, Challenge)
|
[certTypes.infosecQa]: getIdsForCert$(infosecQaId, Challenge)
|
||||||
};
|
};
|
||||||
|
|
||||||
router.post(
|
const superBlocks = Object.keys(superBlockCertTypeMap);
|
||||||
'/certificate/verify/front-end',
|
|
||||||
ifNoUser401,
|
|
||||||
verifyCert.bind(null, certTypes.frontEnd)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/certificate/verify/back-end',
|
'/certificate/verify',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
verifyCert.bind(null, certTypes.backEnd)
|
ifNoSuperBlock404,
|
||||||
);
|
verifyCert
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/certificate/verify/responsive-web-design',
|
|
||||||
ifNoUser401,
|
|
||||||
verifyCert.bind(null, certTypes.respWebDesign)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/certificate/verify/front-end-libraries',
|
|
||||||
ifNoUser401,
|
|
||||||
verifyCert.bind(null, certTypes.frontEndLibs)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/certificate/verify/javascript-algorithms-data-structures',
|
|
||||||
ifNoUser401,
|
|
||||||
verifyCert.bind(null, certTypes.jsAlgoDataStruct)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/certificate/verify/data-visualization',
|
|
||||||
ifNoUser401,
|
|
||||||
verifyCert.bind(null, certTypes.dataVis)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/certificate/verify/apis-microservices',
|
|
||||||
ifNoUser401,
|
|
||||||
verifyCert.bind(null, certTypes.apisMicroservices)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/certificate/verify/information-security-quality-assurance',
|
|
||||||
ifNoUser401,
|
|
||||||
verifyCert.bind(null, certTypes.infosecQa)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/certificate/honest',
|
|
||||||
sendMessageToNonUser,
|
|
||||||
postHonest
|
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
function verifyCert(certType, req, res, next) {
|
const noNameMessage = dedent`
|
||||||
const { user } = req;
|
We need your name so we can put it on your certificate.
|
||||||
|
Add your name to your account settings and click the save button.
|
||||||
|
Then we can issue your certificate.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const notCertifiedMessage = name => dedent`
|
||||||
|
it looks like you have not completed the neccessary steps.
|
||||||
|
Please complete the required challenges to claim the
|
||||||
|
${name}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const alreadyClaimedMessage = name => dedent`
|
||||||
|
It looks like you already have claimed the ${name}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const successMessage = (username, name) => dedent`
|
||||||
|
@${username}, you have sucessfully claimed
|
||||||
|
the ${name}!
|
||||||
|
Congratulations on behalf of the freeCodeCamp team!
|
||||||
|
`;
|
||||||
|
|
||||||
|
function verifyCert(req, res, next) {
|
||||||
|
const { body: { superBlock }, user } = req;
|
||||||
|
|
||||||
|
let certType = superBlockCertTypeMap[superBlock];
|
||||||
|
log(certType);
|
||||||
|
if (certType === 'isDataVisCert') {
|
||||||
|
certType = 'is2018DataVisCert';
|
||||||
|
log(certType);
|
||||||
|
}
|
||||||
return user.getChallengeMap$()
|
return user.getChallengeMap$()
|
||||||
.flatMap(() => certTypeIds[certType])
|
.flatMap(() => certTypeIds[certType])
|
||||||
.flatMap(challenge => {
|
.flatMap(challenge => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
tests,
|
tests,
|
||||||
name,
|
name,
|
||||||
challengeType
|
challengeType
|
||||||
} = challenge;
|
} = challenge;
|
||||||
if (
|
if (user[certType]) {
|
||||||
user[certType] ||
|
return Observable.just(alreadyClaimedMessage(name));
|
||||||
!isCertified(tests, user.challengeMap)
|
}
|
||||||
) {
|
if (!user[certType] && !isCertified(tests, user.challengeMap)) {
|
||||||
return Observable.just(false);
|
return Observable.just(notCertifiedMessage(name));
|
||||||
|
}
|
||||||
|
if (!user.name) {
|
||||||
|
return Observable.just(noNameMessage);
|
||||||
}
|
}
|
||||||
const updateData = {
|
const updateData = {
|
||||||
$set: {
|
$set: {
|
||||||
@ -232,49 +223,32 @@ export default function certificate(app) {
|
|||||||
sendCertifiedEmail(user, Email.send$),
|
sendCertifiedEmail(user, Email.send$),
|
||||||
({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage })
|
({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage })
|
||||||
)
|
)
|
||||||
.map(
|
.map(
|
||||||
({ count, pledgeOrMessage }) => {
|
({ count, pledgeOrMessage }) => {
|
||||||
if (typeof pledgeOrMessage === 'string') {
|
if (typeof pledgeOrMessage === 'string') {
|
||||||
log(pledgeOrMessage);
|
log(pledgeOrMessage);
|
||||||
}
|
}
|
||||||
log(`${count} documents updated`);
|
log(`${count} documents updated`);
|
||||||
return true;
|
return successMessage(user.username, name);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe(
|
||||||
(didCertify) => {
|
(message) => {
|
||||||
if (didCertify) {
|
return res.status(200).json({
|
||||||
// Check if they have a name set
|
message,
|
||||||
if (user.name === '') {
|
success: message.includes('Congratulations')
|
||||||
return res.status(200).send(
|
});
|
||||||
dedent`
|
|
||||||
We need your name so we can put it on your certificate.
|
|
||||||
<a href="https://github.com/settings/profile">Add your
|
|
||||||
name to your GitHub account</a>, then go to your
|
|
||||||
<a href="https://www.freecodecamp.org/settings">settings
|
|
||||||
page</a> and click the "update my portfolio from GitHub"
|
|
||||||
button. Then we can issue your certificate.
|
|
||||||
`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return res.status(200).send(true);
|
|
||||||
}
|
|
||||||
return res.status(200).send(
|
|
||||||
dedent`
|
|
||||||
Looks like you have not completed the neccessary steps.
|
|
||||||
Please return to the challenge map.
|
|
||||||
`
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
next
|
next
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function postHonest(req, res, next) {
|
function ifNoSuperBlock404(req, res, next) {
|
||||||
return req.user.update$({ $set: { isHonest: true } }).subscribe(
|
const { superBlock } = req.body;
|
||||||
() => res.status(200).send(true),
|
if (superBlock && superBlocks.includes(superBlock)) {
|
||||||
next
|
return next();
|
||||||
);
|
}
|
||||||
|
return res.status(404).end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import { alertTypes } from '../../common/utils/flash.js';
|
|||||||
|
|
||||||
export default function settingsController(app) {
|
export default function settingsController(app) {
|
||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
const toggleUserFlag = flag => (req, res, next) => {
|
const toggleUserFlag = (flag, req, res, next) => {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
const currentValue = user[ flag ];
|
const currentValue = user[ flag ];
|
||||||
return user
|
return user
|
||||||
@ -24,6 +24,15 @@ export default function settingsController(app) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function refetchChallengeMap(req, res, next) {
|
||||||
|
const { user } = req;
|
||||||
|
return user.requestChallengeMap()
|
||||||
|
.subscribe(
|
||||||
|
challengeMap => res.json({ challengeMap }),
|
||||||
|
next
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const updateMyEmailValidators = [
|
const updateMyEmailValidators = [
|
||||||
check('email')
|
check('email')
|
||||||
.isEmail()
|
.isEmail()
|
||||||
@ -39,14 +48,6 @@ export default function settingsController(app) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
api.post(
|
|
||||||
'/update-my-email',
|
|
||||||
ifNoUser401,
|
|
||||||
updateMyEmailValidators,
|
|
||||||
createValidatorErrorHandler(alertTypes.danger),
|
|
||||||
updateMyEmail
|
|
||||||
);
|
|
||||||
|
|
||||||
function updateMyLang(req, res, next) {
|
function updateMyLang(req, res, next) {
|
||||||
const { user, body: { lang } = {} } = req;
|
const { user, body: { lang } = {} } = req;
|
||||||
const langName = supportedLanguages[lang];
|
const langName = supportedLanguages[lang];
|
||||||
@ -87,6 +88,94 @@ export default function settingsController(app) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateMyThemeValidators = [
|
||||||
|
check('theme')
|
||||||
|
.isIn(Object.keys(themes))
|
||||||
|
.withMessage('Theme is invalid.')
|
||||||
|
];
|
||||||
|
|
||||||
|
function updateMyTheme(req, res, next) {
|
||||||
|
const { body: { theme } } = req;
|
||||||
|
if (req.user.theme === theme) {
|
||||||
|
return res.sendFlash(alertTypes.info, 'Theme already set');
|
||||||
|
}
|
||||||
|
return req.user.updateTheme(theme)
|
||||||
|
.then(
|
||||||
|
() => res.sendFlash(alertTypes.info, 'Your theme has been updated'),
|
||||||
|
next
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFlags(req, res, next) {
|
||||||
|
const { user, body: { values } } = req;
|
||||||
|
const keys = Object.keys(values);
|
||||||
|
if (
|
||||||
|
keys.length === 1 &&
|
||||||
|
typeof keys[0] === 'boolean'
|
||||||
|
) {
|
||||||
|
return toggleUserFlag(keys[0], req, res, next);
|
||||||
|
}
|
||||||
|
return user.requestUpdateFlags(values)
|
||||||
|
.subscribe(
|
||||||
|
message => res.json({ message }),
|
||||||
|
next
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMyPortfolio(req, res, next) {
|
||||||
|
const {
|
||||||
|
user,
|
||||||
|
body: { portfolio }
|
||||||
|
} = req;
|
||||||
|
// if we only have one key, it should be the id
|
||||||
|
// user cannot send only one key to this route
|
||||||
|
// other than to remove a portfolio item
|
||||||
|
const requestDelete = Object.keys(portfolio).length === 1;
|
||||||
|
return user.updateMyPortfolio(portfolio, requestDelete)
|
||||||
|
.subscribe(
|
||||||
|
message => res.json({ message }),
|
||||||
|
next
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMyProjects(req, res, next) {
|
||||||
|
const {
|
||||||
|
user,
|
||||||
|
body: { projects: project }
|
||||||
|
} = req;
|
||||||
|
return user.updateMyProjects(project)
|
||||||
|
.subscribe(
|
||||||
|
message => res.json({ message }),
|
||||||
|
next
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMyUsername(req, res, next) {
|
||||||
|
const { user, body: { username } } = req;
|
||||||
|
return user.updateMyUsername(username)
|
||||||
|
.subscribe(
|
||||||
|
message => res.json({ message }),
|
||||||
|
next
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
api.post(
|
||||||
|
'/refetch-user-challenge-map',
|
||||||
|
ifNoUser401,
|
||||||
|
refetchChallengeMap
|
||||||
|
);
|
||||||
|
api.post(
|
||||||
|
'/update-flags',
|
||||||
|
ifNoUser401,
|
||||||
|
updateFlags
|
||||||
|
);
|
||||||
|
api.post(
|
||||||
|
'/update-my-email',
|
||||||
|
ifNoUser401,
|
||||||
|
updateMyEmailValidators,
|
||||||
|
createValidatorErrorHandler(alertTypes.danger),
|
||||||
|
updateMyEmail
|
||||||
|
);
|
||||||
api.post(
|
api.post(
|
||||||
'/update-my-current-challenge',
|
'/update-my-current-challenge',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
@ -94,23 +183,21 @@ export default function settingsController(app) {
|
|||||||
createValidatorErrorHandler(alertTypes.danger),
|
createValidatorErrorHandler(alertTypes.danger),
|
||||||
updateMyCurrentChallenge
|
updateMyCurrentChallenge
|
||||||
);
|
);
|
||||||
|
api.post(
|
||||||
const updateMyThemeValidators = [
|
'/update-my-lang',
|
||||||
check('theme')
|
ifNoUser401,
|
||||||
.isIn(Object.keys(themes))
|
updateMyLang
|
||||||
.withMessage('Theme is invalid.')
|
);
|
||||||
];
|
api.post(
|
||||||
function updateMyTheme(req, res, next) {
|
'/update-my-portfolio',
|
||||||
const { body: { theme } } = req;
|
ifNoUser401,
|
||||||
if (req.user.theme === theme) {
|
updateMyPortfolio
|
||||||
return res.sendFlash(alertTypes.info, 'Theme already set');
|
);
|
||||||
}
|
api.post(
|
||||||
return req.user.updateTheme(theme)
|
'/update-my-projects',
|
||||||
.then(
|
ifNoUser401,
|
||||||
() => res.sendFlash(alertTypes.info, 'Your theme has been updated'),
|
updateMyProjects
|
||||||
next
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
api.post(
|
api.post(
|
||||||
'/update-my-theme',
|
'/update-my-theme',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
@ -118,36 +205,10 @@ export default function settingsController(app) {
|
|||||||
createValidatorErrorHandler(alertTypes.danger),
|
createValidatorErrorHandler(alertTypes.danger),
|
||||||
updateMyTheme
|
updateMyTheme
|
||||||
);
|
);
|
||||||
|
|
||||||
api.post(
|
api.post(
|
||||||
'/toggle-available-for-hire',
|
'/update-my-username',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
toggleUserFlag('isAvailableForHire')
|
updateMyUsername
|
||||||
);
|
|
||||||
api.post(
|
|
||||||
'/toggle-lockdown',
|
|
||||||
ifNoUser401,
|
|
||||||
toggleUserFlag('isLocked')
|
|
||||||
);
|
|
||||||
api.post(
|
|
||||||
'/toggle-announcement-email',
|
|
||||||
ifNoUser401,
|
|
||||||
toggleUserFlag('sendMonthlyEmail')
|
|
||||||
);
|
|
||||||
api.post(
|
|
||||||
'/toggle-notification-email',
|
|
||||||
ifNoUser401,
|
|
||||||
toggleUserFlag('sendNotificationEmail')
|
|
||||||
);
|
|
||||||
api.post(
|
|
||||||
'/toggle-quincy-email',
|
|
||||||
ifNoUser401,
|
|
||||||
toggleUserFlag('sendQuincyEmail')
|
|
||||||
);
|
|
||||||
api.post(
|
|
||||||
'/update-my-lang',
|
|
||||||
ifNoUser401,
|
|
||||||
updateMyLang
|
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(api);
|
app.use(api);
|
||||||
|
@ -2,6 +2,7 @@ import dedent from 'dedent';
|
|||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
// import { curry } from 'lodash';
|
||||||
import emoji from 'node-emoji';
|
import emoji from 'node-emoji';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -11,10 +12,12 @@ import {
|
|||||||
frontEndLibsId,
|
frontEndLibsId,
|
||||||
jsAlgoDataStructId,
|
jsAlgoDataStructId,
|
||||||
dataVisId,
|
dataVisId,
|
||||||
|
dataVis2018Id,
|
||||||
apisMicroservicesId,
|
apisMicroservicesId,
|
||||||
infosecQaId
|
infosecQaId
|
||||||
} from '../utils/constantStrings.json';
|
} from '../utils/constantStrings.json';
|
||||||
import certTypes from '../utils/certTypes.json';
|
import certTypes from '../utils/certTypes.json';
|
||||||
|
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
|
||||||
import {
|
import {
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
ifNoUserRedirectTo,
|
ifNoUserRedirectTo,
|
||||||
@ -32,6 +35,7 @@ import { getChallengeInfo, cachedMap } from '../utils/map';
|
|||||||
|
|
||||||
const debug = debugFactory('fcc:boot:user');
|
const debug = debugFactory('fcc:boot:user');
|
||||||
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
||||||
|
// const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map');
|
||||||
const certIds = {
|
const certIds = {
|
||||||
[certTypes.frontEnd]: frontEndChallengeId,
|
[certTypes.frontEnd]: frontEndChallengeId,
|
||||||
[certTypes.backEnd]: backEndChallengeId,
|
[certTypes.backEnd]: backEndChallengeId,
|
||||||
@ -39,6 +43,7 @@ const certIds = {
|
|||||||
[certTypes.frontEndLibs]: frontEndLibsId,
|
[certTypes.frontEndLibs]: frontEndLibsId,
|
||||||
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
|
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
|
||||||
[certTypes.dataVis]: dataVisId,
|
[certTypes.dataVis]: dataVisId,
|
||||||
|
[certTypes.dataVis2018]: dataVis2018Id,
|
||||||
[certTypes.apisMicroservices]: apisMicroservicesId,
|
[certTypes.apisMicroservices]: apisMicroservicesId,
|
||||||
[certTypes.infosecQa]: infosecQaId
|
[certTypes.infosecQa]: infosecQaId
|
||||||
};
|
};
|
||||||
@ -52,6 +57,7 @@ const certViews = {
|
|||||||
[certTypes.jsAlgoDataStruct]:
|
[certTypes.jsAlgoDataStruct]:
|
||||||
'certificate/javascript-algorithms-and-data-structures.jade',
|
'certificate/javascript-algorithms-and-data-structures.jade',
|
||||||
[certTypes.dataVis]: 'certificate/data-visualization.jade',
|
[certTypes.dataVis]: 'certificate/data-visualization.jade',
|
||||||
|
[certTypes.dataVis2018]: 'certificate/data-visualization-2018.jade',
|
||||||
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
|
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
|
||||||
[certTypes.infosecQa]:
|
[certTypes.infosecQa]:
|
||||||
'certificate/information-security-and-quality-assurance.jade'
|
'certificate/information-security-and-quality-assurance.jade'
|
||||||
@ -66,6 +72,7 @@ const certText = {
|
|||||||
[certTypes.jsAlgoDataStruct]:
|
[certTypes.jsAlgoDataStruct]:
|
||||||
'JavaScript Algorithms and Data Structures Certified',
|
'JavaScript Algorithms and Data Structures Certified',
|
||||||
[certTypes.dataVis]: 'Data Visualization Certified',
|
[certTypes.dataVis]: 'Data Visualization Certified',
|
||||||
|
[certTypes.dataVis2018]: 'Data Visualization Certified',
|
||||||
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
|
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
|
||||||
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
|
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
|
||||||
};
|
};
|
||||||
@ -160,11 +167,6 @@ module.exports = function(app) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/delete-my-account',
|
|
||||||
sendNonUserToMap,
|
|
||||||
showDelete
|
|
||||||
);
|
|
||||||
api.post(
|
api.post(
|
||||||
'/account/delete',
|
'/account/delete',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
@ -175,17 +177,11 @@ module.exports = function(app) {
|
|||||||
sendNonUserToMap,
|
sendNonUserToMap,
|
||||||
getAccount
|
getAccount
|
||||||
);
|
);
|
||||||
router.get(
|
|
||||||
'/reset-my-progress',
|
|
||||||
sendNonUserToMap,
|
|
||||||
showResetProgress
|
|
||||||
);
|
|
||||||
api.post(
|
api.post(
|
||||||
'/account/resetprogress',
|
'/account/reset-progress',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
postResetProgress
|
postResetProgress
|
||||||
);
|
);
|
||||||
|
|
||||||
api.get(
|
api.get(
|
||||||
'/account/unlink/:social',
|
'/account/unlink/:social',
|
||||||
sendNonUserToMap,
|
sendNonUserToMap,
|
||||||
@ -194,48 +190,8 @@ module.exports = function(app) {
|
|||||||
|
|
||||||
// Ensure these are the last routes!
|
// Ensure these are the last routes!
|
||||||
api.get(
|
api.get(
|
||||||
'/:username/front-end-certification',
|
'/c/:username/:cert',
|
||||||
showCert.bind(null, certTypes.frontEnd)
|
showCert
|
||||||
);
|
|
||||||
|
|
||||||
api.get(
|
|
||||||
'/:username/back-end-certification',
|
|
||||||
showCert.bind(null, certTypes.backEnd)
|
|
||||||
);
|
|
||||||
|
|
||||||
api.get(
|
|
||||||
'/:username/full-stack-certification',
|
|
||||||
(req, res) => res.redirect(req.url.replace('full-stack', 'back-end'))
|
|
||||||
);
|
|
||||||
|
|
||||||
api.get(
|
|
||||||
'/:username/responsive-web-design-certification',
|
|
||||||
showCert.bind(null, certTypes.respWebDesign)
|
|
||||||
);
|
|
||||||
|
|
||||||
api.get(
|
|
||||||
'/:username/front-end-libraries-certification',
|
|
||||||
showCert.bind(null, certTypes.frontEndLibs)
|
|
||||||
);
|
|
||||||
|
|
||||||
api.get(
|
|
||||||
'/:username/javascript-algorithms-data-structures-certification',
|
|
||||||
showCert.bind(null, certTypes.jsAlgoDataStruct)
|
|
||||||
);
|
|
||||||
|
|
||||||
api.get(
|
|
||||||
'/:username/data-visualization-certification',
|
|
||||||
showCert.bind(null, certTypes.dataVis)
|
|
||||||
);
|
|
||||||
|
|
||||||
api.get(
|
|
||||||
'/:username/apis-microservices-certification',
|
|
||||||
showCert.bind(null, certTypes.apisMicroservices)
|
|
||||||
);
|
|
||||||
|
|
||||||
api.get(
|
|
||||||
'/:username/information-security-quality-assurance-certification',
|
|
||||||
showCert.bind(null, certTypes.infosecQa)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get('/:username', showUserProfile);
|
router.get('/:username', showUserProfile);
|
||||||
@ -410,14 +366,14 @@ module.exports = function(app) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCert(certType, req, res, next) {
|
function showCert(req, res, next) {
|
||||||
const username = req.params.username.toLowerCase();
|
let { username, cert } = req.params;
|
||||||
|
username = username.toLowerCase();
|
||||||
|
const certType = superBlockCertTypeMap[cert];
|
||||||
const certId = certIds[certType];
|
const certId = certIds[certType];
|
||||||
return findUserByUsername$(username, {
|
return findUserByUsername$(username, {
|
||||||
isGithubCool: true,
|
|
||||||
isCheater: true,
|
isCheater: true,
|
||||||
isLocked: true,
|
isLocked: true,
|
||||||
isAvailableForHire: true,
|
|
||||||
isFrontEndCert: true,
|
isFrontEndCert: true,
|
||||||
isBackEndCert: true,
|
isBackEndCert: true,
|
||||||
isFullStackCert: true,
|
isFullStackCert: true,
|
||||||
@ -425,6 +381,7 @@ module.exports = function(app) {
|
|||||||
isFrontEndLibsCert: true,
|
isFrontEndLibsCert: true,
|
||||||
isJsAlgoDataStructCert: true,
|
isJsAlgoDataStructCert: true,
|
||||||
isDataVisCert: true,
|
isDataVisCert: true,
|
||||||
|
is2018DataVisCert: true,
|
||||||
isApisMicroservicesCert: true,
|
isApisMicroservicesCert: true,
|
||||||
isInfosecQaCert: true,
|
isInfosecQaCert: true,
|
||||||
isHonest: true,
|
isHonest: true,
|
||||||
@ -434,6 +391,7 @@ module.exports = function(app) {
|
|||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe(
|
||||||
user => {
|
user => {
|
||||||
|
const profile = `/${user.username}`;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
req.flash(
|
req.flash(
|
||||||
'danger',
|
'danger',
|
||||||
@ -441,15 +399,16 @@ module.exports = function(app) {
|
|||||||
);
|
);
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
if (!user.isGithubCool) {
|
|
||||||
|
if (!user.name) {
|
||||||
req.flash(
|
req.flash(
|
||||||
'danger',
|
'danger',
|
||||||
dedent`
|
dedent`
|
||||||
This user needs to link GitHub with their account
|
This user needs to add their name to their account
|
||||||
in order for others to be able to view their certificate.
|
in order for others to be able to view their certificate.
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
return res.redirect('back');
|
return res.redirect(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isCheater) {
|
if (user.isCheater) {
|
||||||
@ -465,20 +424,20 @@ module.exports = function(app) {
|
|||||||
in order for others to be able to view their certificate.
|
in order for others to be able to view their certificate.
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
return res.redirect('back');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.isHonest) {
|
if (!user.isHonest) {
|
||||||
req.flash(
|
req.flash(
|
||||||
'danger',
|
'danger',
|
||||||
dedent`
|
dedent`
|
||||||
${username} has not yet agreed to our Academic Honesty Pledge.
|
${username} has not yet agreed to our Academic Honesty Pledge.
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
return res.redirect('back');
|
return res.redirect(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user[certType]) {
|
if (user[certType]) {
|
||||||
|
|
||||||
const { challengeMap = {} } = user;
|
const { challengeMap = {} } = user;
|
||||||
const { completedDate = new Date() } = challengeMap[certId] || {};
|
const { completedDate = new Date() } = challengeMap[certId] || {};
|
||||||
|
|
||||||
@ -495,51 +454,49 @@ module.exports = function(app) {
|
|||||||
'danger',
|
'danger',
|
||||||
`Looks like user ${username} is not ${certText[certType]}`
|
`Looks like user ${username} is not ${certText[certType]}`
|
||||||
);
|
);
|
||||||
return res.redirect('back');
|
return res.redirect(profile);
|
||||||
},
|
},
|
||||||
next
|
next
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDelete(req, res) {
|
|
||||||
return res.render('account/delete', { title: 'Delete My Account!' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function postDeleteAccount(req, res, next) {
|
function postDeleteAccount(req, res, next) {
|
||||||
User.destroyById(req.user.id, function(err) {
|
User.destroyById(req.user.id, function(err) {
|
||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
req.logout();
|
req.logout();
|
||||||
req.flash('info', 'You\'ve successfully deleted your account.');
|
req.flash('success', 'You have successfully deleted your account.');
|
||||||
return res.redirect('/');
|
return res.status(200).end();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showResetProgress(req, res) {
|
|
||||||
return res.render('account/reset-progress', { title: 'Reset My Progress!'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function postResetProgress(req, res, next) {
|
function postResetProgress(req, res, next) {
|
||||||
User.findById(req.user.id, function(err, user) {
|
User.findById(req.user.id, function(err, user) {
|
||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
return user.updateAttributes({
|
return user.update$({
|
||||||
progressTimestamps: [{
|
progressTimestamps: [{
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
}],
|
}],
|
||||||
currentStreak: 0,
|
|
||||||
longestStreak: 0,
|
|
||||||
currentChallengeId: '',
|
currentChallengeId: '',
|
||||||
isBackEndCert: false,
|
isRespWebDesignCert: false,
|
||||||
isFullStackCert: false,
|
is2018DataVisCert: false,
|
||||||
isDataVisCert: false,
|
isFrontEndLibsCert: false,
|
||||||
|
isJsAlgoDataStructCert: false,
|
||||||
|
isApisMicroservicesCert: false,
|
||||||
|
isInfosecQaCert: false,
|
||||||
|
is2018FullStackCert: false,
|
||||||
isFrontEndCert: false,
|
isFrontEndCert: false,
|
||||||
challengeMap: {},
|
isBackEndCert: false,
|
||||||
challegesCompleted: []
|
isDataVisCert: false,
|
||||||
}, function(err) {
|
isFullStackCert: false,
|
||||||
if (err) { return next(err); }
|
challengeMap: {}
|
||||||
req.flash('info', 'You\'ve successfully reset your progress.');
|
})
|
||||||
return res.redirect('/');
|
.subscribe(
|
||||||
});
|
() => {
|
||||||
|
req.flash('success', 'You have successfully reset your progress.');
|
||||||
|
return res.status(200).end();
|
||||||
|
},
|
||||||
|
next
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,44 +1,10 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
// import debug from 'debug';
|
|
||||||
// use old rxjs
|
|
||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
const publicUserProps = [
|
import {
|
||||||
'id',
|
userPropsForSession,
|
||||||
'name',
|
normaliseUserFields
|
||||||
'username',
|
} from '../utils/publicUserProps';
|
||||||
'bio',
|
|
||||||
'theme',
|
|
||||||
'picture',
|
|
||||||
'points',
|
|
||||||
'email',
|
|
||||||
'languageTag',
|
|
||||||
|
|
||||||
'isCheater',
|
|
||||||
'isGithubCool',
|
|
||||||
|
|
||||||
'isLocked',
|
|
||||||
'isAvailableForHire',
|
|
||||||
'isFrontEndCert',
|
|
||||||
'isBackEndCert',
|
|
||||||
'isDataVisCert',
|
|
||||||
'isFullStackCert',
|
|
||||||
'isRespWebDesignCert',
|
|
||||||
'isFrontEndLibsCert',
|
|
||||||
'isJsAlgoDataStructCert',
|
|
||||||
'isApisMicroservicesCert',
|
|
||||||
'isInfosecQaCert',
|
|
||||||
|
|
||||||
'githubURL',
|
|
||||||
'sendMonthlyEmail',
|
|
||||||
'sendNotificationEmail',
|
|
||||||
'sendQuincyEmail',
|
|
||||||
|
|
||||||
'currentChallengeId',
|
|
||||||
'challengeMap'
|
|
||||||
];
|
|
||||||
|
|
||||||
// const log = debug('fcc:services:user');
|
|
||||||
|
|
||||||
export default function userServices() {
|
export default function userServices() {
|
||||||
return {
|
return {
|
||||||
@ -51,18 +17,23 @@ export default function userServices() {
|
|||||||
Observable.defer(() => user.getChallengeMap$())
|
Observable.defer(() => user.getChallengeMap$())
|
||||||
.map(challengeMap => ({ ...user.toJSON(), challengeMap }))
|
.map(challengeMap => ({ ...user.toJSON(), challengeMap }))
|
||||||
.map(user => ({
|
.map(user => ({
|
||||||
entities: {
|
entities: {
|
||||||
user: {
|
user: {
|
||||||
[user.username]: {
|
[user.username]: {
|
||||||
..._.pick(user, publicUserProps),
|
..._.pick(user, userPropsForSession),
|
||||||
isTwitter: !!user.twitter,
|
isEmailVerified: !!user.emailVerified,
|
||||||
isLinkedIn: !!user.linkedIn
|
isGithub: !!user.githubURL,
|
||||||
|
isLinkedIn: !!user.linkedIn,
|
||||||
|
isTwitter: !!user.twitter,
|
||||||
|
isWebsite: !!user.website,
|
||||||
|
...normaliseUserFields(user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
result: user.username
|
||||||
result: user.username
|
})
|
||||||
}))
|
)
|
||||||
)
|
)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
user => cb(null, user),
|
user => cb(null, user),
|
||||||
cb
|
cb
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"frontEnd": "isFrontEndCert",
|
"frontEnd": "isFrontEndCert",
|
||||||
"backEnd": "isBackEndCert",
|
"backEnd": "isBackEndCert",
|
||||||
|
"dataVis": "isDataVisCert",
|
||||||
"fullStack": "isFullStackCert",
|
"fullStack": "isFullStackCert",
|
||||||
"respWebDesign": "isRespWebDesignCert",
|
"respWebDesign": "isRespWebDesignCert",
|
||||||
"frontEndLibs": "isFrontEndLibsCert",
|
"frontEndLibs": "isFrontEndLibsCert",
|
||||||
|
"dataVis2018": "is2018DataVisCert",
|
||||||
"jsAlgoDataStruct": "isJsAlgoDataStructCert",
|
"jsAlgoDataStruct": "isJsAlgoDataStructCert",
|
||||||
"dataVis": "isDataVisCert",
|
|
||||||
"apisMicroservices": "isApisMicroservicesCert",
|
"apisMicroservices": "isApisMicroservicesCert",
|
||||||
"infosecQa": "isInfosecQaCert"
|
"infosecQa": "isInfosecQaCert"
|
||||||
}
|
}
|
@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"gitHubUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36",
|
"gitHubUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36",
|
||||||
|
|
||||||
"frontEndChallengeId": "561add10cb82ac38a17513be",
|
"frontEndChallengeId": "561add10cb82ac38a17513be",
|
||||||
"backEndChallengeId": "660add10cb82ac38a17513be",
|
"backEndChallengeId": "660add10cb82ac38a17513be",
|
||||||
|
"dataVisId": "561add10cb82ac39a17513bc",
|
||||||
|
|
||||||
"respWebDesignId": "561add10cb82ac38a17513bc",
|
"respWebDesignId": "561add10cb82ac38a17513bc",
|
||||||
"frontEndLibsId": "561acd10cb82ac38a17513bc",
|
"frontEndLibsId": "561acd10cb82ac38a17513bc",
|
||||||
|
"dataVis2018Id": "5a553ca864b52e1d8bceea14",
|
||||||
"jsAlgoDataStructId": "561abd10cb81ac38a17513bc",
|
"jsAlgoDataStructId": "561abd10cb81ac38a17513bc",
|
||||||
"dataVisId": "561add10cb82ac39a17513bc",
|
|
||||||
"apisMicroservicesId": "561add10cb82ac38a17523bc",
|
"apisMicroservicesId": "561add10cb82ac38a17523bc",
|
||||||
"infosecQaId": "561add10cb82ac38a17213bc"
|
"infosecQaId": "561add10cb82ac38a17213bc"
|
||||||
}
|
}
|
||||||
|
@ -20,3 +20,7 @@ export function unDasherize(name) {
|
|||||||
.replace(/[^a-zA-Z\d\s]/g, '')
|
.replace(/[^a-zA-Z\d\s]/g, '')
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addPlaceholderImage(name) {
|
||||||
|
return `https://identicon.org?t=${name}&s=256`;
|
||||||
|
}
|
||||||
|
85
server/utils/publicUserProps.js
Normal file
85
server/utils/publicUserProps.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { isURL } from 'validator';
|
||||||
|
|
||||||
|
import { addPlaceholderImage } from '../utils';
|
||||||
|
import {
|
||||||
|
prepUniqueDaysByHours,
|
||||||
|
calcCurrentStreak,
|
||||||
|
calcLongestStreak
|
||||||
|
} from '../utils/user-stats';
|
||||||
|
|
||||||
|
export const publicUserProps = [
|
||||||
|
'about',
|
||||||
|
'calendar',
|
||||||
|
'challengeMap',
|
||||||
|
'githubURL',
|
||||||
|
'isApisMicroservicesCert',
|
||||||
|
'isBackEndCert',
|
||||||
|
'isCheater',
|
||||||
|
'isDataVisCert',
|
||||||
|
'isFrontEndCert',
|
||||||
|
'isFullStackCert',
|
||||||
|
'isFrontEndLibsCert',
|
||||||
|
'isGithubCool',
|
||||||
|
'isHonest',
|
||||||
|
'isInfosecQaCert',
|
||||||
|
'isJsAlgoDataStructCert',
|
||||||
|
'isLocked',
|
||||||
|
'isRespWebDesignCert',
|
||||||
|
'linkedin',
|
||||||
|
'location',
|
||||||
|
'name',
|
||||||
|
'points',
|
||||||
|
'portfolio',
|
||||||
|
'projects',
|
||||||
|
'streak',
|
||||||
|
'twitter',
|
||||||
|
'username',
|
||||||
|
'website'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const userPropsForSession = [
|
||||||
|
...publicUserProps,
|
||||||
|
'currentChallengeId',
|
||||||
|
'email',
|
||||||
|
'id',
|
||||||
|
'languageTag',
|
||||||
|
'sendQuincyEmail',
|
||||||
|
'theme'
|
||||||
|
];
|
||||||
|
|
||||||
|
export function normaliseUserFields(user) {
|
||||||
|
const about = user.bio && !user.about ? user.bio : user.about;
|
||||||
|
const picture = user.picture || addPlaceholderImage(user.username);
|
||||||
|
const twitter = user.twitter && isURL(user.twitter) ?
|
||||||
|
user.twitter :
|
||||||
|
user.twitter && `https://www.twitter.com/${user.twitter.replace(/^@/, '')}`;
|
||||||
|
return { about, picture, twitter };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProgress(progressTimestamps, timezone = 'EST') {
|
||||||
|
const calendar = progressTimestamps
|
||||||
|
.map((objOrNum) => {
|
||||||
|
return typeof objOrNum === 'number' ?
|
||||||
|
objOrNum :
|
||||||
|
objOrNum.timestamp;
|
||||||
|
})
|
||||||
|
.filter((timestamp) => {
|
||||||
|
return !!timestamp;
|
||||||
|
})
|
||||||
|
.reduce((data, timeStamp) => {
|
||||||
|
data[Math.floor(timeStamp / 1000)] = 1;
|
||||||
|
return data;
|
||||||
|
}, {});
|
||||||
|
const timestamps = progressTimestamps
|
||||||
|
.map(objOrNum => {
|
||||||
|
return typeof objOrNum === 'number' ?
|
||||||
|
objOrNum :
|
||||||
|
objOrNum.timestamp;
|
||||||
|
});
|
||||||
|
const uniqueHours = prepUniqueDaysByHours(timestamps, timezone);
|
||||||
|
const streak = {
|
||||||
|
longest: calcLongestStreak(uniqueHours, timezone),
|
||||||
|
current: calcCurrentStreak(uniqueHours, timezone)
|
||||||
|
};
|
||||||
|
return { calendar, streak };
|
||||||
|
}
|
19
server/utils/superBlockCertTypeMap.js
Normal file
19
server/utils/superBlockCertTypeMap.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import certTypes from './certTypes.json';
|
||||||
|
|
||||||
|
const superBlockCertTypeMap = {
|
||||||
|
// legacy
|
||||||
|
'front-end': certTypes.frontEnd,
|
||||||
|
'back-end': certTypes.backEnd,
|
||||||
|
'data-visualization': certTypes.dataVis,
|
||||||
|
'full-stack': certTypes.fullStack,
|
||||||
|
|
||||||
|
// modern
|
||||||
|
'responsive-web-design': certTypes.respWebDesign,
|
||||||
|
'javascript-algorithms-and-data-structures': certTypes.jsAlgoDataStruct,
|
||||||
|
'front-end-libraries': certTypes.frontEndLibs,
|
||||||
|
'data-visualization-2018': certTypes.dataVis2018,
|
||||||
|
'apis-and-microservices': certTypes.apisMicroservices,
|
||||||
|
'information-security-and-quality-assurance': certTypes.infosecQa
|
||||||
|
};
|
||||||
|
|
||||||
|
export default superBlockCertTypeMap;
|
@ -1,8 +1,8 @@
|
|||||||
Thank you for updating your contact details.
|
Thank you for updating your contact details.
|
||||||
|
|
||||||
Please verify your email by following the link below:
|
Please verify your new email by following the link below:
|
||||||
|
|
||||||
<a href="<%= verifyHref %>"><%= verifyHref %></a>
|
<%= host %>/passwordless-change?email=<%= loginEmail %>&token=<%= loginToken %>&emailChange=<%= emailChange %>
|
||||||
|
|
||||||
Happy coding!
|
Happy coding!
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user