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:
Stuart Taylor
2018-02-16 23:18:53 +00:00
committed by Quincy Larson
parent 9f034f4f79
commit 24ef69cf7a
78 changed files with 4395 additions and 1724 deletions

View File

@ -26,9 +26,9 @@
.content-mixin(baseline) { align-content: baseline; }
.content-mixin(@_) {}
.grid(@direction: row; @items: none; @justify: none; @content: none) {
.grid(@direction: row; @items: none; @justify: none; @content: none; @wrap: wrap) {
display: flex;
flex-wrap: wrap;
flex-wrap: @wrap;
flex-direction: @direction;
.justify-mixin(@justify);

View File

@ -37,7 +37,7 @@ export default handleActions(
[types.makeToast]: (state, { payload: toast }) => [
...state,
toast
],
].filter(toast => !!toast.message),
[types.removeToast]: (state, { payload: key }) => state.filter(
toast => toast.key !== key
)

View File

@ -7,7 +7,7 @@
// Here we invert the order in which
// they are painted using css so the
// nav is on top again
.grid(@direction: column);
.grid(@direction: column; @wrap: nowrap);
height: 100%;
width: 100%;
}

View File

@ -1,4 +1,5 @@
import _ from 'lodash';
import { findIndex, invert, pick, property } from 'lodash';
import uuid from 'uuid/v4';
import {
composeReducers,
createAction,
@ -8,11 +9,16 @@ import {
import { themes } from '../../utils/themes';
import { types as challenges } from '../routes/Challenges/redux';
import { usernameSelector } from '../redux';
export const ns = 'entities';
export const getNS = state => state[ns];
export const entitiesSelector = getNS;
export const types = createTypes([
'addPortfolioItem',
'optoUpdatePortfolio',
'regresPortfolio',
'updateMultipleUserFlags',
'updateTheme',
'updateUserFlag',
'updateUserEmail',
@ -20,6 +26,18 @@ export const types = createTypes([
'updateUserCurrentChallenge'
], 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
export const updateUserFlag = createAction(
types.updateUserFlag,
@ -41,7 +59,8 @@ export const updateUserCurrentChallenge = createAction(
);
// entity meta creators
const getEntityAction = _.property('meta.entitiesAction');
const getEntityAction = property('meta.entitiesAction');
export const updateThemeMetacreator = (username, theme) => ({
entitiesAction: {
type: types.updateTheme,
@ -52,6 +71,16 @@ export const updateThemeMetacreator = (username, theme) => ({
}
});
export function emptyPortfolio() {
return {
id: uuid(),
title: '',
description: '',
url: '',
image: ''
};
}
const defaultState = {
superBlock: {},
block: {},
@ -59,13 +88,56 @@ const defaultState = {
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 function makeBlockSelector(block) {
return state => {
const blockMap = getNS(state).block || {};
return blockMap[block] || {};
};
}
export function makeSuperBlockSelector(name) {
return state => {
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 } }) => ({
...state,
user: {

View File

@ -0,0 +1,11 @@
import React from 'react';
function ButtonSpacer() {
return (
<div className='button-spacer' />
);
}
ButtonSpacer.displayName = 'ButtonSpacer';
export default ButtonSpacer;

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

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

View File

@ -0,0 +1,11 @@
import React from 'react';
function Spacer() {
return (
<div className='spacer' />
);
}
Spacer.displayName = 'Spacer';
export default Spacer;

View 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';

View File

@ -54,11 +54,14 @@ export function fetchChallengesEpic(
{ getState },
{ services }
) {
return actions::ofType(types.appMounted)
return actions::ofType(
types.appMounted,
types.updateChallenges
)
.flatMapLatest(() => {
const lang = langSelector(getState());
const options = {
lang,
params: { lang },
service: 'map'
};
return services.readService$(options)

View File

@ -40,7 +40,7 @@ export const types = createTypes([
createAsyncTypes('fetchChallenge'),
createAsyncTypes('fetchChallenges'),
'updateChallenges',
createAsyncTypes('fetchUser'),
'showSignIn',
@ -108,7 +108,7 @@ export const fetchChallengesCompleted = createAction(
(entities, result) => ({ entities, result }),
entities => ({ entities })
);
export const updateChallenges = createAction(types.updateChallenges);
// updateTitle(title: String) => Action
export const updateTitle = createAction(types.updateTitle);

View File

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

View File

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

View File

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

View File

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

View File

@ -3,58 +3,39 @@ import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Button, Row, Col } from 'react-bootstrap';
import { Button } from 'react-bootstrap';
import FA from 'react-fontawesome';
import ns from './ns.json';
import LockedSettings from './Locked-Settings.jsx';
import JobSettings from './Job-Settings.jsx';
import SocialSettings from './Social-Settings.jsx';
import EmailSettings from './Email-Setting.jsx';
import LanguageSettings from './Language-Settings.jsx';
import SettingsSkeleton from './Settings-Skeleton.jsx';
import { FullWidthRow, Spacer, Loader } from '../../helperComponents';
import AboutSettings from './components/About-Settings.jsx';
import InternetSettings from './components/Internet-Settings.jsx';
import EmailSettings from './components/Email-Settings.jsx';
import DangerZone from './components/DangerZone.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 {
toggleNightMode,
updateTitle,
signInLoadingSelector,
userSelector,
usernameSelector,
themeSelector,
hardGoTo
} from '../../redux';
const mapStateToProps = createSelector(
userSelector,
usernameSelector,
themeSelector,
signInLoadingSelector,
(
{
username,
email,
isAvailableForHire,
isLocked,
isGithubCool,
isTwitter,
isLinkedIn,
sendMonthlyEmail,
sendNotificationEmail,
sendQuincyEmail
},
username,
theme,
showLoading,
) => ({
currentTheme: theme,
email,
isAvailableForHire,
isGithubCool,
isLinkedIn,
isLocked,
isTwitter,
sendMonthlyEmail,
sendNotificationEmail,
sendQuincyEmail,
showLoading,
username
})
@ -62,37 +43,13 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = {
hardGoTo,
toggleIsAvailableForHire: () => toggleUserFlag('isAvailableForHire'),
toggleIsLocked: () => toggleUserFlag('isLocked'),
toggleMonthlyEmail: () => toggleUserFlag('sendMonthlyEmail'),
toggleNightMode,
toggleNotificationEmail: () => toggleUserFlag('sendNotificationEmail'),
toggleQuincyEmail: () => toggleUserFlag('sendQuincyEmail'),
updateTitle
};
const propTypes = {
children: PropTypes.element,
currentTheme: PropTypes.string,
email: PropTypes.string,
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,
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,
updateTitle: PropTypes.func.isRequired,
username: PropTypes.string
@ -121,188 +78,51 @@ export class Settings extends React.Component {
render() {
const {
currentTheme,
email,
isAvailableForHire,
isGithubCool,
isLinkedIn,
isLocked,
isTwitter,
sendMonthlyEmail,
sendNotificationEmail,
sendQuincyEmail,
showLoading,
toggleIsAvailableForHire,
toggleIsLocked,
toggleMonthlyEmail,
toggleNightMode,
toggleNotificationEmail,
toggleQuincyEmail,
username
} = this.props;
if (!username && showLoading) {
return <SettingsSkeleton />;
return <Loader />;
}
return (
<div className={ `${ns}-container` }>
<Row>
<Col xs={ 12 }>
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
className='btn-link-social'
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 }
<FullWidthRow>
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
className='btn-link-social'
href={ `/${username}` }
>
<LockedSettings
isLocked={ isLocked }
toggle={ toggleIsLocked }
/>
</Col>
</Row>
<div className='spacer' />
<h2 className='text-center'>Job Search Settings</h2>
<Row>
<Col
md={ 6 }
mdOffset={ 3 }
sm={ 8 }
smOffset={ 2 }
xs={ 12 }
<FA name='user' />
Show me my public profile
</Button>
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
className='btn-link-social'
href={ '/signout' }
>
<JobSettings
isAvailableForHire={ isAvailableForHire }
toggle={ toggleIsAvailableForHire }
/>
</Col>
</Row>
<div className='spacer' />
<h2 className='text-center'>Email Settings</h2>
<Row>
<Col
md={ 6 }
mdOffset={ 3 }
sm={ 8 }
smOffset={ 2 }
xs={ 12 }
>
<EmailSettings
email={ email }
sendMonthlyEmail={ sendMonthlyEmail }
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>
Sign me out of freeCodeCamp
</Button>
</FullWidthRow>
<h1 className='text-center'>{ `Account Settings for ${username}` }</h1>
<AboutSettings />
<Spacer />
<EmailSettings />
<Spacer />
<LanguageSettings />
<Spacer />
<InternetSettings />
<Spacer />
<PortfolioSettings />
<Spacer />
<CertificationSettings />
<Spacer />
<Honesty />
<Spacer />
<DangerZone />
</div>
);
}

View File

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

View 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);

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

View 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);

View 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);

View 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: &thinsp;
<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;

View 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&apos;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);

View 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);

View 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);

View 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);

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

View File

@ -2,11 +2,19 @@ import PropTypes from 'prop-types';
import React from 'react';
import { createSelector } from 'reselect';
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 { userSelector } from '../../redux';
import langs from '../../../utils/supported-languages';
import { updateMyLang } from '../redux';
import { userSelector } from '../../../redux';
import langs from '../../../../utils/supported-languages';
import { FullWidthRow } from '../../../helperComponents';
import SectionHeader from './SectionHeader.jsx';
const propTypes = {
fields: PropTypes.object,
@ -85,18 +93,35 @@ export class LanguageSettings extends React.Component {
fields: { lang: { name, value } }
} = this.props;
return (
<FormGroup>
<FormControl
className='btn btn-block btn-primary btn-link-social btn-lg'
componentClass='select'
name={ name }
onChange={ this.handleChange }
style={{ height: '45px' }}
value={ value }
>
{ options }
</FormControl>
</FormGroup>
<div>
<SectionHeader>
Language Settings
</SectionHeader>
<FullWidthRow>
<FormGroup>
<Row>
<Col sm={ 4 } xs={ 12 }>
<ControlLabel htmlFor={ name }>
Prefered Language for Challenges
</ControlLabel>
</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>
);
}
}

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

View 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);

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

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

View 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);

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

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

View 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);

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

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

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

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

View 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';
}

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

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

View File

@ -1,23 +1,46 @@
import { isLocationAction } from 'redux-first-router';
import {
addNS,
composeReducers,
createAction,
createAsyncTypes,
createTypes
createTypes,
handleActions
} 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 { utils } from '../../../Flash/redux';
export const epics = [
certificateEpic,
dangerZoneEpic,
newUsernameEpic,
userUpdateEpic
];
const createActionWithFlash = type => createAction(
type,
null,
utils.createFlashMetaAction
);
export const types = createTypes([
'toggleUserFlag',
createAsyncTypes('claimCert'),
createAsyncTypes('updateMyEmail'),
createAsyncTypes('updateUserBackend'),
createAsyncTypes('deletePortfolio'),
createAsyncTypes('updateMyPortfolio'),
'updateMyLang',
'updateNewUsernameValidity',
createAsyncTypes('validateUsername'),
createAsyncTypes('refetchChallengeMap'),
createAsyncTypes('deleteAccount'),
createAsyncTypes('resetProgress'),
'onRouteSettings',
'onRouteUpdateEmail'
], 'settings');
@ -25,34 +48,92 @@ export const types = createTypes([
export const onRouteSettings = createAction(types.onRouteSettings);
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 updateMyEmailComplete = createAction(
types.updateMyEmail.complete,
null,
utils.createFlashMetaAction
export const updateMyEmailComplete = createActionWithFlash(
types.updateMyEmail.complete
);
export const updateMyEmailError = createActionWithFlash(
types.updateMyEmail.error
);
export const updateMyEmailError = createAction(
types.updateMyEmail.error,
null,
utils.createFlashMetaAction
export const updateMyPortfolio = createAction(types.updateMyPortfolio.start);
export const updateMyPortfolioComplete = createAction(
types.updateMyPortfolio.complete
);
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(
types.updateMyLang,
(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 = {
showUpdateEmailView: false
showUpdateEmailView: false,
isValidUsername: false,
validating: false
};
const getNS = state => state[ns];
export function settingsSelector(state) {
return getNS(state);
}
export const showUpdateEmailViewSelector =
state => getNS(state).showUpdateEmailView;
export default addNS(
export default composeReducers(
ns,
function settingsRouteReducer(state = defaultState, action) {
if (isLocationAction(action)) {
@ -71,5 +152,20 @@ export default addNS(
}
}
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
)
);

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

View File

@ -1,31 +1,176 @@
import { Observable } from 'rx';
import { combineEpics, ofType } from 'redux-epic';
import { types, onRouteSettings } from './';
import { pick } from 'lodash';
import {
types,
onRouteSettings,
refetchChallengeMap,
updateUserBackendComplete,
updateMyPortfolioComplete
} from './';
import { makeToast } from '../../../Toasts/redux';
import {
fetchChallenges,
updateChallenges,
doActionOnError,
userSelector
usernameSelector,
userSelector,
createErrorObservable
} from '../../../redux';
import {
updateUserFlag,
updateUserEmail,
updateUserLang
updateUserLang,
updateMultipleUserFlags,
regresPortfolio,
optoUpdatePortfolio
} from '../../../entities';
import { postJSON$ } from '../../../../utils/ajax-stream';
import langs from '../../../../utils/supported-languages';
const urlMap = {
isLocked: 'lockdown',
isAvailableForHire: 'available-for-hire',
sendQuincyEmail: 'quincy-email',
sendNotificationEmail: 'notification-email',
sendMonthlyEmail: 'announcement-email'
const endpoints = {
email: '/update-my-email',
projects: '/update-my-projects',
username: '/update-my-username'
};
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)
.flatMap(({ payload: email }) => {
const {
@ -38,7 +183,6 @@ export function updateUserEmailEpic(actions, { getState }) {
updateUserEmail(username, email)
);
const ajaxUpdate = postJSON$('/update-my-email', body)
.map(({ message }) => makeToast({ message }))
.catch(doActionOnError(() => oldEmail ?
updateUserEmail(username, oldEmail) :
null
@ -71,7 +215,7 @@ export function updateUserLangEpic(actions, { getState }) {
// update url to reflect change
onRouteSettings({ lang }),
// refetch challenges in new language
fetchChallenges()
updateChallenges()
);
})
.catch(doActionOnError(() => {
@ -85,41 +229,11 @@ export function updateUserLangEpic(actions, { getState }) {
});
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(
updateUserFlagEpic,
backendUserUpdateEpic,
refetchChallengeMapEpic,
updateMyPortfolioEpic,
updateUserEmailEpic,
updateUserLangEpic
);

View File

@ -3,20 +3,46 @@
@skeleton-gray: #b0bdb7;
@keyframes pulsingOverlay {
0% {
opacity: 0.5;
}
50% {
opacity: 0.8;
}
100% {
opacity: 0.5;
.night .@{ns}-container {
.btn-group {
label:disabled, label[disabled] {
border: 1px solid #999;
background-color: #999;
color: #333;
}
}
}
.@{ns}-container {
.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 {
@ -25,23 +51,41 @@
})
}
.@{ns}-skeleton {
background-color: #fff;
z-index: 10;
animation-name: pulsingOverlay;
animation-duration: 2.5s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: normal;
.inline-form-field {
display: flex;
align-items: center;
margin: 5px 0;
.placeholder-string {
background-color: @skeleton-gray;
box-shadow: 0px 0px 12px 6px @skeleton-gray;
color: @skeleton-gray;
}
.btn-link-social {
background-color: @skeleton-gray;
border-color: @skeleton-gray;
box-shadow: 0px 0px 12px 6px @skeleton-gray;
input, textarea {
background-color: #fff;
}
}
.edit-preview-tabs {
li {
padding-bottom: 0px;
}
}
.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;
}
}

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

View File

@ -6,17 +6,16 @@ import debugFactory from 'debug';
import { isEmail } from 'validator';
import path from 'path';
import loopback from 'loopback';
import _ from 'lodash';
import { themes } from '../utils/themes';
import { dasherize } from '../../server/utils';
import { saveUser, observeMethod } from '../../server/utils/rx.js';
import { blacklistedUsernames } from '../../server/utils/constants.js';
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
import {
getServerFullURL,
getEmailSender,
getProtocol,
getHost,
getPort
getEmailSender
} from '../../server/utils/url-utils.js';
const debug = debugFactory('fcc:models:user');
@ -38,6 +37,61 @@ function destroyAll(id, Model) {
)({ 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(
__dirname,
'..',
@ -58,6 +112,16 @@ const renderSignInEmail = loopback.template(path.join(
'user-request-sign-in.ejs'
));
const renderEmailChangeEmail = loopback.template(path.join(
__dirname,
'..',
'..',
'server',
'views',
'emails',
'user-request-update-email.ejs'
));
function getAboutProfile({
username,
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$()
.do(accessToken => {
const config = {
@ -297,11 +366,19 @@ module.exports = function(User) {
res.cookie('userId', accessToken.userId, config);
}
});
const updateUser = this.update$({
let data = {
emailVerified: true,
emailAuthLinkTTL: null,
emailVerifyTTL: null
});
};
if (emailChange && this.newEmail) {
data = {
...data,
email: this.newEmail,
newEmail: null
};
}
const updateUser = this.update$(data);
return Observable.combineLatest(
createToken,
updateUser,
@ -425,7 +502,7 @@ module.exports = function(User) {
User.decodeEmail = email => Buffer(email, 'base64').toString();
User.prototype.requestAuthEmail = function requestAuthEmail(isSignUp) {
function requestAuthEmail(isSignUp, newEmail) {
return Observable.defer(() => {
const messageOrNull = getWaitMessage(this.emailAuthLinkTTL);
if (messageOrNull) {
@ -448,22 +525,26 @@ module.exports = function(User) {
renderAuthEmail = renderSignUpEmail;
subject = 'Account Created - freeCodeCamp';
}
if (newEmail) {
renderAuthEmail = renderEmailChangeEmail;
subject = 'Email Change Request - freeCodeCamp';
}
const { id: loginToken, created: emailAuthLinkTTL } = token;
const loginEmail = this.getEncodedEmail();
const loginEmail = this.getEncodedEmail(newEmail ? newEmail : null);
const host = getServerFullURL();
const mailOptions = {
type: 'email',
to: this.email,
to: newEmail ? newEmail : this.email,
from: getEmailSender(),
subject,
text: renderAuthEmail({
host,
loginEmail,
loginToken
loginToken,
emailChange: !!newEmail
})
};
return Observable.combineLatest(
return Observable.forkJoin(
User.email.send$(mailOptions),
this.update$({ emailAuthLinkTTL })
);
@ -479,17 +560,19 @@ module.exports = function(User) {
Please follow that link to sign in.
`
);
};
}
User.prototype.requestAuthEmail = requestAuthEmail;
User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) {
const currentEmail = this.email;
return Observable.defer(() => {
const ownEmail = newEmail === this.email;
if (!isEmail('' + newEmail)) {
throw createEmailError();
}
// email is already associated and verified with this account
if (ownEmail) {
const isOwnEmail = isTheSame(newEmail, currentEmail);
const sameUpdate = isTheSame(newEmail, this.newEmail);
const messageOrNull = getWaitMessage(this.emailVerifyTTL);
if (isOwnEmail) {
if (this.emailVerified) {
// email is already associated and verified with this account
throw wrapHandledError(
new Error('email is already verified'),
{
@ -497,10 +580,8 @@ module.exports = function(User) {
message: `${newEmail} is already associated with this account.`
}
);
} else {
const messageOrNull = getWaitMessage(this.emailVerifyTTL);
// email is already associated but unverified
if (messageOrNull) {
} else if (!this.emailVerified && messageOrNull) {
// email is associated but unverified and
// email is within time limit
throw wrapHandledError(
new Error(),
@ -510,69 +591,175 @@ module.exports = function(User) {
}
);
}
}
}
// at this point email is not associated with the account
// or has not been verified but user is requesting another token
// outside of the time limit
if (sameUpdate && messageOrNull) {
// trying to update with the same newEmail and
// confirmation email is still valid
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(
() => ownEmail,
() => isOwnEmail || (sameUpdate && messageOrNull),
Observable.empty(),
// defer prevents the promise from firing prematurely (before subscribe)
Observable.defer(() => User.doesExist(null, newEmail))
)
.do(exists => {
// not associated with this account, but is associated with another
if (exists) {
throw wrapHandledError(
new Error('email already in use'),
{
type: 'info',
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));
.do(exists => {
if (exists) {
// newEmail is not associated with this account,
// but is associated with different account
throw wrapHandledError(
new Error('email already in use'),
{
type: 'info',
message:
`${newEmail} is already associated with another account.`
}
);
}
})
.flatMap(() => {
const mailOptions = {
type: 'email',
to: newEmail,
from: getEmailSender(),
subject: 'freeCodeCamp - Email Update Requested',
protocol: getProtocol(),
host: getHost(),
port: getPort(),
template: path.join(
__dirname,
'..',
'..',
'server',
'views',
'emails',
'user-request-update-email.ejs'
)
};
return this.verify(mailOptions);
const update = {
newEmail,
emailVerified: false,
emailVerifyTTL: new Date()
};
return this.update$(update)
.do(() => Object.assign(this, update))
.flatMap(() => this.requestAuthEmail(false, newEmail));
});
});
};
User.prototype.requestChallengeMap = function requestChallengeMap() {
return this.getChallengeMap$();
};
User.prototype.requestUpdateFlags = function requestUpdateFlags(values) {
const flagsToCheck = Object.keys(values);
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`
Please check your email.
We sent you a link that you can click to verify your email address.
We have successfully updated your account.
`);
};
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 =
function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) {
const findUser = observeMethod(User, 'findOne');

View File

@ -89,6 +89,10 @@
"type": "string",
"default": ""
},
"about": {
"type": "string",
"default": ""
},
"name": {
"type": "string",
"default": ""
@ -139,11 +143,6 @@
"description": "Campers profile does not show challenges/certificates to the public",
"default": false
},
"isAvailableForHire": {
"type": "boolean",
"description": "Camper is available for hire",
"default": false
},
"currentChallengeId": {
"type": "string",
"description": "The challenge last visited by the user",
@ -185,12 +184,12 @@
},
"isRespWebDesignCert": {
"type": "boolean",
"description": "Camper is data visualization certified",
"description": "Camper is responsive web design certified",
"default": false
},
"isNewDataVisCert": {
"is2018DataVisCert": {
"type": "boolean",
"description": "Camper is responsive web design certified",
"description": "Camper is data visualization certified (2018)",
"default": false
},
"isFrontEndLibsCert": {
@ -243,6 +242,10 @@
],
"default": []
},
"portfolio": {
"type": "array",
"default": []
},
"rand": {
"type": "number",
"index": true

View 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 elses 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 freeCodeCamps team will audit my projects ' +
'to confirm this.',
'In the situations where we discover instances of unambiguous plagiarism, ' +
'we will replace the camper in questions 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
View File

@ -2914,6 +2914,17 @@
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
"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": {
"version": "0.3.0",
"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",
"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": {
"version": "0.1.4",
"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",
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz",
@ -5434,6 +5456,11 @@
"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": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@ -5575,6 +5602,13 @@
"@types/express": "4.0.39",
"lodash": "4.17.4",
"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": {
@ -7488,6 +7522,15 @@
"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": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz",
@ -13543,6 +13586,14 @@
"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": {
"version": "0.1.8",
"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": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/react-hot-api/-/react-hot-api-0.4.7.tgz",
@ -14089,6 +14151,15 @@
"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": {
"version": "15.6.2",
"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": {
"version": "5.4.1",
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@ -17066,6 +17148,12 @@
"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": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz",
@ -17844,9 +17932,9 @@
}
},
"validator": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz",
"integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA=="
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-9.4.0.tgz",
"integrity": "sha512-ftkCYp/7HrGdybVCuwSje07POAd93ksZJpb5GVDBzm8SLKIm3QMJcZugb5dOJsONBoWhIXl0jtoGHTyou3DAgA=="
},
"value-equal": {
"version": "0.4.0",

View File

@ -55,6 +55,7 @@
"cors": "^2.8.4",
"csurf": "^1.8.3",
"d3": "~3.5.17",
"date-fns": "^1.29.0",
"debug": "^2.2.0",
"dedent": "~0.7.0",
"dotenv": "^4.0.0",
@ -105,6 +106,7 @@
"passport-oauth": "^1.0.0",
"passport-twitter": "^1.0.3",
"pmx": "~0.6.2",
"prismjs": "^1.11.0",
"prop-types": "^15.5.10",
"react": "^15.6.2",
"react-addons-css-transition-group": "~15.4.2",
@ -114,6 +116,7 @@
"react-dom": "^15.6.2",
"react-fontawesome": "^1.2.0",
"react-freecodecamp-search": "^1.4.1",
"react-helmet": "^5.2.0",
"react-images": "^0.5.1",
"react-motion": "~0.4.2",
"react-no-ssr": "^1.0.1",
@ -135,7 +138,7 @@
"snyk": "^1.68.1",
"store": "git+https://github.com/berkeleytrue/store.js.git#feature/noop-server",
"uuid": "^3.0.1",
"validator": "^8.2.0"
"validator": "^9.4.0"
},
"devDependencies": {
"adler32": "~0.1.7",

74
public/css/loader.css Normal file
View 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
View 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;
}

View File

@ -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 elses 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",
""
]
]
}
}
}
]
}

View File

@ -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 elses 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.",
""
]
]
}
}
}
]
}

View File

@ -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 elses 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.",
""
]
]
}
}
}
]
}

View File

@ -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 elses 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.",
""
]
]
}
}
}
]
}

View File

@ -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 elses 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.",
""
]
]
}
}
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,8 +8,8 @@ const _ = require('lodash');
const utils = require('../server/utils');
const getChallenges = require('./getChallenges');
const app = require('../server/server');
const createDebugger = require('debug');
const log = createDebugger('fcc:seed');
// force logger to always output
// this may be brittle
@ -51,6 +51,7 @@ Observable.combineLatest(
const message = challengeSpec.message;
const required = challengeSpec.required || [];
const template = challengeSpec.template;
const isPrivate = !!challengeSpec.isPrivate;
log('parsed %s successfully', blockName);
@ -63,12 +64,13 @@ Observable.combineLatest(
title: blockName,
name: nameify(blockName),
dashedName: dasherize(blockName),
superOrder: superOrder,
superBlock: superBlock,
superOrder,
superBlock,
superBlockMessage: message,
order: order,
time: time,
isLocked: isLocked
order,
time,
isLocked,
isPrivate
};
return createBlocks(block)
@ -110,6 +112,7 @@ Observable.combineLatest(
challenge.isBeta = challenge.isBeta || isBeta;
challenge.isComingSoon = challenge.isComingSoon || isComingSoon;
challenge.isLocked = challenge.isLocked || isLocked;
challenge.isPrivate = challenge.isPrivate || isPrivate;
challenge.time = challengeSpec.time;
challenge.superOrder = superOrder;
challenge.superBlock = superBlock

View File

@ -7,9 +7,11 @@ import { check } from 'express-validator/check';
import {
ifUserRedirectTo,
ifNoUserRedirectTo,
createValidatorErrorHandler
} from '../utils/middleware';
import { wrapHandledError } from '../utils/create-handled-error.js';
import { homeURL } from '../../common/utils/constantStrings.json';
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
// 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
app.enableAuth();
const ifUserRedirect = ifUserRedirectTo();
const ifNoUserRedirectHome = ifNoUserRedirectTo(homeURL);
const router = app.loopback.Router();
const api = app.loopback.Router();
const { AuthToken, User } = app.models;
@ -79,7 +82,8 @@ module.exports = function enableAuthentication(app) {
const {
query: {
email: encodedEmail,
token: authTokenId
token: authTokenId,
emailChange
} = {}
} = req;
@ -122,14 +126,16 @@ module.exports = function enableAuthentication(app) {
);
}
if (user.email !== email) {
throw wrapHandledError(
new Error('user email does not match'),
{
type: 'info',
message: defaultErrorMsg,
redirectTo: '/email-signin'
}
);
if (!emailChange || (emailChange && user.newEmail !== email)) {
throw wrapHandledError(
new Error('user email does not match'),
{
type: 'info',
message: defaultErrorMsg,
redirectTo: '/email-signin'
}
);
}
}
return authToken.validate$()
.map(isValid => {
@ -185,6 +191,13 @@ module.exports = function enableAuthentication(app) {
getPasswordlessAuth
);
router.get(
'/passwordless-change',
ifNoUserRedirectHome,
passwordlessGetValidators,
getPasswordlessAuth
);
const passwordlessPostValidators = [
check('email')
.isEmail()

View File

@ -7,28 +7,31 @@ import debug from 'debug';
import { isEmail } from 'validator';
import {
ifNoUser401,
ifNoUserSend
ifNoUser401
} from '../utils/middleware';
import { observeQuery } from '../utils/rx';
import {
// legacy
frontEndChallengeId,
backEndChallengeId,
dataVisId,
// modern
respWebDesignId,
frontEndLibsId,
dataVis2018Id,
jsAlgoDataStructId,
frontEndChallengeId,
dataVisId,
apisMicroservicesId,
backEndChallengeId,
infosecQaId
} from '../utils/constantStrings.json';
import {
completeCommitment$
} from '../utils/commit';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
const log = debug('fcc:certification');
const renderCertifedEmail = loopback.template(path.join(
@ -38,12 +41,9 @@ const renderCertifedEmail = loopback.template(path.join(
'emails',
'certified.ejs'
));
const sendMessageToNonUser = ifNoUserSend(
'must be logged in to complete.'
);
function isCertified(ids, challengeMap = {}) {
return _.every(ids, ({ id }) => challengeMap[id]);
return _.every(ids, ({ id }) => _.has(challengeMap, id));
}
function getIdsForCert$(id, Challenge) {
@ -120,12 +120,16 @@ export default function certificate(app) {
const { Email, Challenge } = app.models;
const certTypeIds = {
// legacy
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge),
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge),
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
// modern
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
[certTypes.frontEndLibs]: getIdsForCert$(frontEndLibsId, Challenge),
[certTypes.dataVis2018]: getIdsForCert$(dataVis2018Id, Challenge),
[certTypes.jsAlgoDataStruct]: getIdsForCert$(jsAlgoDataStructId, Challenge),
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
[certTypes.apisMicroservices]: getIdsForCert$(
apisMicroservicesId,
Challenge
@ -133,78 +137,65 @@ export default function certificate(app) {
[certTypes.infosecQa]: getIdsForCert$(infosecQaId, Challenge)
};
router.post(
'/certificate/verify/front-end',
ifNoUser401,
verifyCert.bind(null, certTypes.frontEnd)
);
const superBlocks = Object.keys(superBlockCertTypeMap);
router.post(
'/certificate/verify/back-end',
'/certificate/verify',
ifNoUser401,
verifyCert.bind(null, certTypes.backEnd)
);
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
ifNoSuperBlock404,
verifyCert
);
app.use(router);
function verifyCert(certType, req, res, next) {
const { user } = req;
const noNameMessage = dedent`
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$()
.flatMap(() => certTypeIds[certType])
.flatMap(challenge => {
.flatMap(() => certTypeIds[certType])
.flatMap(challenge => {
const {
id,
tests,
name,
challengeType
} = challenge;
if (
user[certType] ||
!isCertified(tests, user.challengeMap)
) {
return Observable.just(false);
if (user[certType]) {
return Observable.just(alreadyClaimedMessage(name));
}
if (!user[certType] && !isCertified(tests, user.challengeMap)) {
return Observable.just(notCertifiedMessage(name));
}
if (!user.name) {
return Observable.just(noNameMessage);
}
const updateData = {
$set: {
@ -232,49 +223,32 @@ export default function certificate(app) {
sendCertifiedEmail(user, Email.send$),
({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage })
)
.map(
.map(
({ count, pledgeOrMessage }) => {
if (typeof pledgeOrMessage === 'string') {
log(pledgeOrMessage);
}
log(`${count} documents updated`);
return true;
return successMessage(user.username, name);
}
);
})
})
.subscribe(
(didCertify) => {
if (didCertify) {
// Check if they have a name set
if (user.name === '') {
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.
`
);
(message) => {
return res.status(200).json({
message,
success: message.includes('Congratulations')
});
},
next
);
}
function postHonest(req, res, next) {
return req.user.update$({ $set: { isHonest: true } }).subscribe(
() => res.status(200).send(true),
next
);
function ifNoSuperBlock404(req, res, next) {
const { superBlock } = req.body;
if (superBlock && superBlocks.includes(superBlock)) {
return next();
}
return res.status(404).end();
}
}

View File

@ -10,7 +10,7 @@ import { alertTypes } from '../../common/utils/flash.js';
export default function settingsController(app) {
const api = app.loopback.Router();
const toggleUserFlag = flag => (req, res, next) => {
const toggleUserFlag = (flag, req, res, next) => {
const { user } = req;
const currentValue = user[ flag ];
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 = [
check('email')
.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) {
const { user, body: { lang } = {} } = req;
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(
'/update-my-current-challenge',
ifNoUser401,
@ -94,23 +183,21 @@ export default function settingsController(app) {
createValidatorErrorHandler(alertTypes.danger),
updateMyCurrentChallenge
);
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
);
}
api.post(
'/update-my-lang',
ifNoUser401,
updateMyLang
);
api.post(
'/update-my-portfolio',
ifNoUser401,
updateMyPortfolio
);
api.post(
'/update-my-projects',
ifNoUser401,
updateMyProjects
);
api.post(
'/update-my-theme',
ifNoUser401,
@ -118,36 +205,10 @@ export default function settingsController(app) {
createValidatorErrorHandler(alertTypes.danger),
updateMyTheme
);
api.post(
'/toggle-available-for-hire',
'/update-my-username',
ifNoUser401,
toggleUserFlag('isAvailableForHire')
);
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
updateMyUsername
);
app.use(api);

View File

@ -2,6 +2,7 @@ import dedent from 'dedent';
import moment from 'moment-timezone';
import { Observable } from 'rx';
import debugFactory from 'debug';
// import { curry } from 'lodash';
import emoji from 'node-emoji';
import {
@ -11,10 +12,12 @@ import {
frontEndLibsId,
jsAlgoDataStructId,
dataVisId,
dataVis2018Id,
apisMicroservicesId,
infosecQaId
} from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import {
ifNoUser401,
ifNoUserRedirectTo,
@ -32,6 +35,7 @@ import { getChallengeInfo, cachedMap } from '../utils/map';
const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map');
// const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map');
const certIds = {
[certTypes.frontEnd]: frontEndChallengeId,
[certTypes.backEnd]: backEndChallengeId,
@ -39,6 +43,7 @@ const certIds = {
[certTypes.frontEndLibs]: frontEndLibsId,
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
[certTypes.dataVis]: dataVisId,
[certTypes.dataVis2018]: dataVis2018Id,
[certTypes.apisMicroservices]: apisMicroservicesId,
[certTypes.infosecQa]: infosecQaId
};
@ -52,6 +57,7 @@ const certViews = {
[certTypes.jsAlgoDataStruct]:
'certificate/javascript-algorithms-and-data-structures.jade',
[certTypes.dataVis]: 'certificate/data-visualization.jade',
[certTypes.dataVis2018]: 'certificate/data-visualization-2018.jade',
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
[certTypes.infosecQa]:
'certificate/information-security-and-quality-assurance.jade'
@ -66,6 +72,7 @@ const certText = {
[certTypes.jsAlgoDataStruct]:
'JavaScript Algorithms and Data Structures Certified',
[certTypes.dataVis]: 'Data Visualization Certified',
[certTypes.dataVis2018]: 'Data Visualization Certified',
[certTypes.apisMicroservices]: 'APIs and Microservices 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(
'/account/delete',
ifNoUser401,
@ -175,17 +177,11 @@ module.exports = function(app) {
sendNonUserToMap,
getAccount
);
router.get(
'/reset-my-progress',
sendNonUserToMap,
showResetProgress
);
api.post(
'/account/resetprogress',
'/account/reset-progress',
ifNoUser401,
postResetProgress
);
api.get(
'/account/unlink/:social',
sendNonUserToMap,
@ -194,48 +190,8 @@ module.exports = function(app) {
// Ensure these are the last routes!
api.get(
'/:username/front-end-certification',
showCert.bind(null, certTypes.frontEnd)
);
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)
'/c/:username/:cert',
showCert
);
router.get('/:username', showUserProfile);
@ -410,14 +366,14 @@ module.exports = function(app) {
);
}
function showCert(certType, req, res, next) {
const username = req.params.username.toLowerCase();
function showCert(req, res, next) {
let { username, cert } = req.params;
username = username.toLowerCase();
const certType = superBlockCertTypeMap[cert];
const certId = certIds[certType];
return findUserByUsername$(username, {
isGithubCool: true,
isCheater: true,
isLocked: true,
isAvailableForHire: true,
isFrontEndCert: true,
isBackEndCert: true,
isFullStackCert: true,
@ -425,6 +381,7 @@ module.exports = function(app) {
isFrontEndLibsCert: true,
isJsAlgoDataStructCert: true,
isDataVisCert: true,
is2018DataVisCert: true,
isApisMicroservicesCert: true,
isInfosecQaCert: true,
isHonest: true,
@ -434,6 +391,7 @@ module.exports = function(app) {
})
.subscribe(
user => {
const profile = `/${user.username}`;
if (!user) {
req.flash(
'danger',
@ -441,15 +399,16 @@ module.exports = function(app) {
);
return res.redirect('/');
}
if (!user.isGithubCool) {
if (!user.name) {
req.flash(
'danger',
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.
`
);
return res.redirect('back');
return res.redirect(profile);
}
if (user.isCheater) {
@ -465,20 +424,20 @@ module.exports = function(app) {
in order for others to be able to view their certificate.
`
);
return res.redirect('back');
return res.redirect('/');
}
if (!user.isHonest) {
req.flash(
'danger',
dedent`
dedent`
${username} has not yet agreed to our Academic Honesty Pledge.
`
);
return res.redirect('back');
return res.redirect(profile);
}
if (user[certType]) {
const { challengeMap = {} } = user;
const { completedDate = new Date() } = challengeMap[certId] || {};
@ -495,51 +454,49 @@ module.exports = function(app) {
'danger',
`Looks like user ${username} is not ${certText[certType]}`
);
return res.redirect('back');
return res.redirect(profile);
},
next
);
}
function showDelete(req, res) {
return res.render('account/delete', { title: 'Delete My Account!' });
}
function postDeleteAccount(req, res, next) {
User.destroyById(req.user.id, function(err) {
if (err) { return next(err); }
req.logout();
req.flash('info', 'You\'ve successfully deleted your account.');
return res.redirect('/');
});
}
function showResetProgress(req, res) {
return res.render('account/reset-progress', { title: 'Reset My Progress!'
req.flash('success', 'You have successfully deleted your account.');
return res.status(200).end();
});
}
function postResetProgress(req, res, next) {
User.findById(req.user.id, function(err, user) {
if (err) { return next(err); }
return user.updateAttributes({
return user.update$({
progressTimestamps: [{
timestamp: Date.now()
}],
currentStreak: 0,
longestStreak: 0,
currentChallengeId: '',
isBackEndCert: false,
isFullStackCert: false,
isDataVisCert: false,
isRespWebDesignCert: false,
is2018DataVisCert: false,
isFrontEndLibsCert: false,
isJsAlgoDataStructCert: false,
isApisMicroservicesCert: false,
isInfosecQaCert: false,
is2018FullStackCert: false,
isFrontEndCert: false,
challengeMap: {},
challegesCompleted: []
}, function(err) {
if (err) { return next(err); }
req.flash('info', 'You\'ve successfully reset your progress.');
return res.redirect('/');
});
isBackEndCert: false,
isDataVisCert: false,
isFullStackCert: false,
challengeMap: {}
})
.subscribe(
() => {
req.flash('success', 'You have successfully reset your progress.');
return res.status(200).end();
},
next
);
});
}

View File

@ -1,44 +1,10 @@
import _ from 'lodash';
// import debug from 'debug';
// use old rxjs
import { Observable } from 'rx';
import _ from 'lodash';
const publicUserProps = [
'id',
'name',
'username',
'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');
import {
userPropsForSession,
normaliseUserFields
} from '../utils/publicUserProps';
export default function userServices() {
return {
@ -51,18 +17,23 @@ export default function userServices() {
Observable.defer(() => user.getChallengeMap$())
.map(challengeMap => ({ ...user.toJSON(), challengeMap }))
.map(user => ({
entities: {
user: {
[user.username]: {
..._.pick(user, publicUserProps),
isTwitter: !!user.twitter,
isLinkedIn: !!user.linkedIn
entities: {
user: {
[user.username]: {
..._.pick(user, userPropsForSession),
isEmailVerified: !!user.emailVerified,
isGithub: !!user.githubURL,
isLinkedIn: !!user.linkedIn,
isTwitter: !!user.twitter,
isWebsite: !!user.website,
...normaliseUserFields(user)
}
}
}
},
result: user.username
}))
)
},
result: user.username
})
)
)
.subscribe(
user => cb(null, user),
cb

View File

@ -1,11 +1,12 @@
{
"frontEnd": "isFrontEndCert",
"backEnd": "isBackEndCert",
"dataVis": "isDataVisCert",
"fullStack": "isFullStackCert",
"respWebDesign": "isRespWebDesignCert",
"frontEndLibs": "isFrontEndLibsCert",
"dataVis2018": "is2018DataVisCert",
"jsAlgoDataStruct": "isJsAlgoDataStructCert",
"dataVis": "isDataVisCert",
"apisMicroservices": "isApisMicroservicesCert",
"infosecQa": "isInfosecQaCert"
}

View File

@ -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",
"frontEndChallengeId": "561add10cb82ac38a17513be",
"backEndChallengeId": "660add10cb82ac38a17513be",
"dataVisId": "561add10cb82ac39a17513bc",
"respWebDesignId": "561add10cb82ac38a17513bc",
"frontEndLibsId": "561acd10cb82ac38a17513bc",
"dataVis2018Id": "5a553ca864b52e1d8bceea14",
"jsAlgoDataStructId": "561abd10cb81ac38a17513bc",
"dataVisId": "561add10cb82ac39a17513bc",
"apisMicroservicesId": "561add10cb82ac38a17523bc",
"infosecQaId": "561add10cb82ac38a17213bc"
}

View File

@ -20,3 +20,7 @@ export function unDasherize(name) {
.replace(/[^a-zA-Z\d\s]/g, '')
.trim();
}
export function addPlaceholderImage(name) {
return `https://identicon.org?t=${name}&s=256`;
}

View 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 };
}

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

View File

@ -1,8 +1,8 @@
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!