chore(root): Ensure development environment

This commit is contained in:
Bouncey
2018-09-03 11:23:18 +01:00
committed by mrugesh mohapatra
parent 46a217d0a5
commit dc00eb8555
225 changed files with 164 additions and 21387 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
node_modules node_modules
.env
*lib-cov *lib-cov
*~ *~
*.seed *.seed

View File

@ -310,13 +310,13 @@ mongod
npm run only-once npm run only-once
# Start the application without a backend server # Start the application without a backend server
npm run develop cd ./client && npm run develop
# If you require the backend server to be operational (persisted user interations/api calls) # If you require the backend server to be operational (persisted user interations/api calls)
# Use this command instead # Use this command instead
# Note: This command requires that you have a correctly seeded mongodb instance running # Note: This command requires that you have a correctly seeded mongodb instance running
# Note: If you are runnoing the backend server inside a docker container, use the command above # Note: If you are runnoing the backend server inside a docker container, use the command above
npm run develop-server node develop-client-server.js
``` ```

View File

@ -1,12 +1,17 @@
module.exports = { module.exports = {
plugins: [require.resolve('babel-plugin-transform-function-bind')], plugins: [
require.resolve('babel-plugin-transform-function-bind'),
require.resolve('@babel/plugin-proposal-class-properties'),
require.resolve('@babel/plugin-proposal-object-rest-spread')
],
presets: [ presets: [
[ [
require.resolve('@babel/preset-env'), { require.resolve('@babel/preset-env'),
{
targets: { targets: {
node: 'current' node: '8',
} },
} },
] ],
] ],
} }

View File

@ -22,6 +22,7 @@ tmp
npm-debug.log npm-debug.log
node_modules node_modules
compiled
.idea .idea
*.iml *.iml
.DS_Store .DS_Store

View File

@ -1,86 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import ns from './ns.json';
import {
appMounted,
fetchUser,
isSignedInSelector
} from './redux';
import { fetchMapUi } from './Map/redux';
import Flash from './Flash';
import Nav from './Nav';
import Toasts from './Toasts';
import NotFound from './NotFound';
import { mainRouteSelector } from './routes/redux';
import Profile from './routes/Profile';
import Settings from './routes/Settings';
const mapDispatchToProps = {
appMounted,
fetchMapUi,
fetchUser
};
const mapStateToProps = state => {
const isSignedIn = isSignedInSelector(state);
const route = mainRouteSelector(state);
return {
toast: state.app.toast,
isSignedIn,
route
};
};
const propTypes = {
appMounted: PropTypes.func.isRequired,
children: PropTypes.node,
fetchMapUi: PropTypes.func.isRequired,
fetchUser: PropTypes.func,
isSignedIn: PropTypes.bool,
route: PropTypes.string,
toast: PropTypes.object
};
const routes = {
profile: Profile,
settings: Settings
};
// export plain class for testing
export class FreeCodeCamp extends React.Component {
componentDidMount() {
this.props.appMounted();
this.props.fetchMapUi();
if (!this.props.isSignedIn) {
this.props.fetchUser();
}
}
render() {
const {
route
} = this.props;
const Child = routes[route] || NotFound;
return (
<div className={ `${ns}-container` }>
<Flash />
<Nav />
<Child />
<Toasts />
</div>
);
}
}
FreeCodeCamp.displayName = 'freeCodeCamp';
FreeCodeCamp.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(FreeCodeCamp);

View File

@ -1,25 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ns from './ns.json';
const propTypes = {
children: PropTypes.node,
isFullWidth: PropTypes.bool
};
export default function ChildContainer({ children, isFullWidth }) {
const contentClassname = classnames({
[`${ns}-content`]: true,
[`${ns}-centered`]: !isFullWidth
});
return (
<div className={ contentClassname }>
{ children }
</div>
);
}
ChildContainer.displayName = 'ChildContainer';
ChildContainer.propTypes = propTypes;

View File

@ -1,43 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CloseButton } from 'react-bootstrap';
import { connect } from 'react-redux';
import ns from './ns.json';
import { alertTypes } from '../../utils/flash.js';
import {
latestMessageSelector,
clickOnClose
} from './redux';
const propTypes = {
clickOnClose: PropTypes.func.isRequired,
message: PropTypes.string,
type: PropTypes.oneOf(Object.keys(alertTypes))
};
const mapStateToProps = latestMessageSelector;
const mapDispatchToProps = { clickOnClose };
export function Flash({ type, clickOnClose, message }) {
if (!message) {
return null;
}
return (
<div className={`${ns}-container bg-${type}`}>
<div className={`${ns}-content`}>
<p className={ `${ns}-message` }>
{ message }
</p>
<CloseButton onClick={ clickOnClose }/>
</div>
</div>
);
}
Flash.displayName = 'Flash';
Flash.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(Flash);

View File

@ -1,20 +0,0 @@
@ns: flash;
.@{ns}-container {
width: 100%;
}
.@{ns}-content {
.center(@value: @container-lg, @padding: @grid-gutter-width);
.row(@justify: around);
}
.@{ns}-message {
flex: 1 0 0px;
color: #37474f;
}
#@{ns}-board {
margin-top: 50px;
}

View File

@ -1 +0,0 @@
export { default } from './Flash.jsx';

View File

@ -1 +0,0 @@
"flash"

View File

@ -1,17 +0,0 @@
import { Observable } from 'rx';
import { ofType } from 'redux-epic';
import {
fetchMessagesComplete,
fetchMessagesError
} from './';
import { types as app } from '../../redux';
import { getJSON$ } from '../../../utils/ajax-stream.js';
export default function getMessagesEpic(actions) {
return actions::ofType(app.appMounted)
.flatMap(() => getJSON$('/api/users/get-messages')
.map(fetchMessagesComplete)
.catch(err => Observable.of(fetchMessagesError(err)))
);
}

View File

@ -1,58 +0,0 @@
import _ from 'lodash/fp';
import {
createTypes,
createAction,
createAsyncTypes,
composeReducers,
handleActions
} from 'berkeleys-redux-utils';
import * as utils from './utils.js';
import getMessagesEpic from './get-messages-epic.js';
import ns from '../ns.json';
// export all the utils
export { utils };
export const epics = [getMessagesEpic];
export const types = createTypes([
'clickOnClose',
createAsyncTypes('fetchMessages')
], ns);
export const clickOnClose = createAction(types.clickOnClose, _.noop);
export const fetchMessagesComplete = createAction(types.fetchMessages.complete);
export const fetchMessagesError = createAction(types.fetchMessages.error);
const defaultState = [];
const getNS = _.property(ns);
export const latestMessageSelector = _.flow(
getNS,
_.head,
_.defaultTo({})
);
export default composeReducers(
ns,
handleActions(
() => ({
[types.clickOnClose]: _.tail,
[types.fetchMessages.complete]: (state, { payload }) => [
...state,
...utils.expressToStack(payload)
]
}),
defaultState,
),
function metaReducer(state = defaultState, action) {
if (utils.isFlashAction(action)) {
const { payload } = utils.getFlashAction(action);
return [
...state,
...payload
];
}
return state;
}
);

View File

@ -1,43 +0,0 @@
import _ from 'lodash/fp';
import { alertTypes, normalizeAlertType } from '../../../utils/flash.js';
// interface ExpressFlash {
// [alertType]: [String...]
// }
// interface StackFlash {
// type: AlertType,
// message: String
// }
export const expressToStack = _.flow(
_.toPairs,
_.flatMap(([ type, messages ]) => messages.map(msg => ({
message: msg,
type: normalizeAlertType(type)
})))
);
export const isExpressFlash = _.flow(
_.keys,
_.every(type => alertTypes[type])
);
export const getFlashAction = _.flow(
_.property('meta'),
_.property('flash')
);
// FlashMessage
// createFlashMetaAction(payload: ExpressFlash|StackFlash
export const createFlashMetaAction = payload => {
if (isExpressFlash(payload)) {
payload = expressToStack(payload);
} else {
payload = [payload];
}
return { flash: { payload } };
};
export const isFlashAction = _.flow(
getFlashAction,
Boolean
);

View File

@ -1,38 +0,0 @@
import { ofType } from 'redux-epic';
import debug from 'debug';
import {
types as appTypes,
createErrorObservable
} from '../../redux';
import { types, fetchMapUiComplete } from './';
import { shapeChallenges } from '../../redux/utils';
const isDev = debug.enabled('fcc:*');
export default function fetchMapUiEpic(
actions,
_,
{ services }
) {
return actions::ofType(
appTypes.appMounted,
types.fetchMapUi.start
)
.flatMapLatest(() => {
const options = {
service: 'map-ui'
};
return services.readService$(options)
.retry(3)
.map(({ entities, ...res }) => ({
entities: shapeChallenges(
entities,
isDev
),
...res
}))
.map(fetchMapUiComplete)
.catch(createErrorObservable);
});
}

View File

@ -1,121 +0,0 @@
import {
createAction,
createAsyncTypes,
createTypes,
handleActions
} from 'berkeleys-redux-utils';
import { createSelector } from 'reselect';
import { capitalize, noop } from 'lodash';
import * as utils from './utils.js';
import {
createEventMetaCreator
} from '../../redux';
import fetchMapUiEpic from './fetch-map-ui-epic';
const ns = 'map';
export const epics = [ fetchMapUiEpic ];
export const types = createTypes([
'onRouteMap',
'initMap',
createAsyncTypes('fetchMapUi'),
'toggleThisPanel',
'isAllCollapsed',
'collapseAll',
'expandAll',
'clickOnChallenge'
], ns);
export const initMap = createAction(types.initMap);
export const fetchMapUi = createAction(types.fetchMapUi.start);
export const fetchMapUiComplete = createAction(types.fetchMapUi.complete);
export const toggleThisPanel = createAction(types.toggleThisPanel);
export const collapseAll = createAction(types.collapseAll);
export const expandAll = createAction(types.expandAll);
export const clickOnChallenge = createAction(
types.clickOnChallenge,
noop,
createEventMetaCreator({
category: capitalize(ns),
action: 'click',
label: types.clickOnChallenge
})
);
const initialState = {
mapUi: { isAllCollapsed: false },
superBlocks: []
};
export const getNS = state => state[ns];
export const allColapsedSelector = state => state[ns].isAllCollapsed;
export const mapSelector = state => getNS(state).mapUi;
export function makePanelOpenSelector(name) {
return createSelector(
mapSelector,
mapUi => {
const node = utils.getNode(mapUi, name);
return node ? node.isOpen : false;
}
);
}
// interface Map{
// children: [...{
// name: (superBlock: String),
// isOpen: Boolean,
// children: [...{
// name: (blockName: String),
// isOpen: Boolean,
// children: [...{
// name: (challengeName: String),
// }]
// }]
// }
// }
export default handleActions(
()=> ({
[types.toggleThisPanel]: (state, { payload: name }) => {
return {
...state,
mapUi: utils.toggleThisPanel(state.mapUi, name)
};
},
[types.collapseAll]: state => {
const mapUi = utils.collapseAllPanels(state.mapUi);
mapUi.isAllCollapsed = true;
return {
...state,
mapUi
};
},
[types.expandAll]: state => {
const mapUi = utils.expandAllPanels(state.mapUi);
mapUi.isAllCollapsed = false;
return {
...state,
mapUi
};
},
[types.fetchMapUi.complete]: (state, { payload }) => {
const { entities, result, initialNode } = payload;
const mapUi = utils.createMapUi(entities, result);
return {
...state,
...result,
mapUi: utils.openPath(mapUi, initialNode)
};
}
}),
initialState,
ns
);

View File

@ -1,231 +0,0 @@
import protect from '../../utils/empty-protector';
const throwIfUndefined = () => {
throw new Error('Challenge does not have a title');
};
export function createSearchTitle(
name = throwIfUndefined(),
challengeMap = {}
) {
return challengeMap[name] || name;
}
// interface Node {
// isHidden: Boolean,
// children: Void|[ ...Node ],
// isOpen?: Boolean
// }
//
// interface MapUi
// {
// children: [...{
// name: (superBlock: String),
// isOpen: Boolean,
// isHidden: Boolean,
// children: [...{
// name: (blockName: String),
// isOpen: Boolean,
// isHidden: Boolean,
// children: [...{
// name: (challengeName: String),
// isHidden: Boolean
// }]
// }]
// }]
// }
export function createMapUi(
{
block: blockMap,
challenge: challengeMap,
superBlock: superBlockMap
} = {},
{ superBlocks } = {}
) {
if (!superBlocks || !superBlockMap || !blockMap) {
return {};
}
return {
children: superBlocks.map(superBlock => {
return {
name: superBlock,
isOpen: false,
isHidden: false,
children: protect(superBlockMap[superBlock]).blocks.map(block => {
return {
name: block,
isOpen: false,
isHidden: false,
children: protect(blockMap[block]).challenges.map(challenge => {
return {
name: challenge,
title: createSearchTitle(challenge, challengeMap),
isHidden: false,
children: null
};
})
};
})
};
})
};
}
// synchronise
// traverseMapUi(
// tree: MapUi|Node,
// update: ((MapUi|Node) => MapUi|Node)
// ) => MapUi|Node
export function traverseMapUi(tree, update) {
let childrenChanged;
if (!Array.isArray(tree.children)) {
return update(tree);
}
const newChildren = tree.children.map(node => {
const newNode = traverseMapUi(node, update);
if (!childrenChanged && newNode !== node) {
childrenChanged = true;
}
return newNode;
});
if (childrenChanged) {
tree = {
...tree,
children: newChildren
};
}
return update(tree);
}
// synchronise
// getNode(tree: MapUi, name: String) => MapUi
export function getNode(tree, name) {
let node;
traverseMapUi(tree, thisNode => {
if (thisNode.name === name) {
node = thisNode;
}
return thisNode;
});
return node;
}
// synchronise
// updateSingelNode(
// tree: MapUi,
// name: String,
// update(MapUi|Node) => MapUi|Node
// ) => MapUi
export function updateSingleNode(tree, name, update) {
return traverseMapUi(tree, node => {
if (name !== node.name) {
return node;
}
return update(node);
});
}
// synchronise
// toggleThisPanel(tree: MapUi, name: String) => MapUi
export function toggleThisPanel(tree, name) {
return updateSingleNode(tree, name, node => {
return {
...node,
isOpen: !node.isOpen
};
});
}
// toggleAllPanels(tree: MapUi, isOpen: Boolean = false ) => MapUi
export function toggleAllPanels(tree, isOpen = false) {
return traverseMapUi(tree, node => {
if (!Array.isArray(node.children) || node.isOpen === isOpen) {
return node;
}
return {
...node,
isOpen
};
});
}
// collapseAllPanels(tree: MapUi) => MapUi
export function collapseAllPanels(tree) {
return toggleAllPanels(tree);
}
// expandAllPanels(tree: MapUi) => MapUi
export function expandAllPanels(tree) {
return toggleAllPanels(tree, true);
}
// synchronise
// updatePath(
// tree: MapUi,
// name: String,
// update(MapUi|Node) => MapUi|Node
// ) => MapUi
export function updatePath(tree, name, pathUpdater) {
const path = [];
let pathFound = false;
const isInPath = node => !!path.find(name => name === node.name);
const traverseMap = (tree, update) => {
if (pathFound) {
return isInPath(tree) ? update(tree) : tree;
}
if (tree.name === name) {
pathFound = true;
return update(tree);
}
let childrenChanged;
if (!Array.isArray(tree.children)) {
return tree;
}
if (tree.name) {
path.push(tree.name);
}
const newChildren = tree.children.map(node => {
const newNode = traverseMap(node, update);
if (!childrenChanged && newNode !== node) {
childrenChanged = true;
}
return newNode;
});
if (childrenChanged) {
tree = {
...tree,
children: newChildren
};
}
if (pathFound && isInPath(tree)) {
return update(tree);
}
path.pop();
return tree;
};
return traverseMap(tree, pathUpdater);
}
// synchronise
// openPath(tree: MapUi, name: String) => MapUi
export function openPath(tree, name) {
return updatePath(tree, name, node => {
if (!Array.isArray(node.children)) {
return node;
}
return { ...node, isOpen: true };
});
}

View File

@ -1,319 +0,0 @@
import test from 'tape';
import sinon from 'sinon';
import {
getNode,
createMapUi,
traverseMapUi,
updateSingleNode,
toggleThisPanel,
expandAllPanels,
collapseAllPanels,
updatePath,
openPath
} from './utils.js';
test('createMapUi', t => {
t.plan(3);
t.test('should return an `{}` when proper args not supplied', t => {
t.plan(3);
t.equal(
Object.keys(createMapUi()).length,
0
);
t.equal(
Object.keys(createMapUi({}, [])).length,
0
);
t.equal(
Object.keys(createMapUi({ superBlock: {} }, [])).length,
0
);
});
t.test('should return a map tree', t => {
const expected = {
children: [{
name: 'superBlockA',
children: [{
name: 'blockA',
children: [{
name: 'challengeA'
}]
}]
}]
};
const actual = createMapUi({
superBlock: {
superBlockA: {
blocks: [
'blockA'
]
}
},
block: {
blockA: {
challenges: [
'challengeA'
]
}
}
},
{ superBlocks: ['superBlockA'] },
{ challengeA: 'ChallengeA title'}
);
t.plan(3);
t.equal(actual.children[0].name, expected.children[0].name);
t.equal(
actual.children[0].children[0].name,
expected.children[0].children[0].name
);
t.equal(
actual.children[0].children[0].children[0].name,
expected.children[0].children[0].children[0].name
);
});
t.test('should protect against malformed data', t => {
t.plan(2);
t.equal(
createMapUi({
superBlock: {},
block: {
blockA: {
challenges: [
'challengeA'
]
}
}
}, { superBlocks: ['superBlockA'] }).children[0].children.length,
0
);
t.equal(
createMapUi({
superBlock: {
superBlockA: {
blocks: [
'blockA'
]
}
},
block: {}
},
{ superBlocks: ['superBlockA'] }).children[0].children[0].children.length,
0
);
});
});
test('traverseMapUi', t => {
t.test('should return tree', t => {
t.plan(2);
const expectedTree = {};
const actaulTree = traverseMapUi(expectedTree, tree => {
t.equal(tree, expectedTree);
return tree;
});
t.equal(actaulTree, expectedTree);
});
t.test('should hit every node', t => {
t.plan(4);
const expected = { children: [{ children: [{}] }] };
const spy = sinon.spy(t => t);
spy.withArgs(expected);
spy.withArgs(expected.children[0]);
spy.withArgs(expected.children[0].children[0]);
traverseMapUi(expected, spy);
t.equal(spy.callCount, 3);
t.ok(spy.withArgs(expected).calledOnce, 'foo');
t.ok(spy.withArgs(expected.children[0]).calledOnce, 'bar');
t.ok(spy.withArgs(expected.children[0].children[0]).calledOnce, 'baz');
});
t.test('should create new object when children change', t => {
t.plan(9);
const expected = { children: [{ bar: true }, {}] };
const actual = traverseMapUi(expected, node => ({ ...node, foo: true }));
t.notEqual(actual, expected);
t.notEqual(actual.children, expected.children);
t.notEqual(actual.children[0], expected.children[0]);
t.notEqual(actual.children[1], expected.children[1]);
t.equal(actual.children[0].bar, expected.children[0].bar);
t.notOk(expected.children[0].foo);
t.notOk(expected.children[1].foo);
t.true(actual.children[0].foo);
t.true(actual.children[1].foo);
});
});
test('getNode', t => {
t.test('should return node', t => {
t.plan(1);
const expected = { name: 'foo' };
const tree = { children: [{ name: 'notfoo' }, expected ] };
const actual = getNode(tree, 'foo');
t.equal(expected, actual);
});
t.test('should returned undefined if not found', t => {
t.plan(1);
const tree = {
children: [ { name: 'foo' }, { children: [ { name: 'bar' } ] } ]
};
const actual = getNode(tree, 'baz');
t.notOk(actual);
});
});
test('updateSingleNode', t => {
t.test('should update single node', t => {
const expected = { name: 'foo' };
const untouched = { name: 'notFoo' };
const actual = updateSingleNode(
{ children: [ untouched, expected ] },
'foo',
node => ({ ...node, tag: true })
);
t.plan(4);
t.ok(actual.children[1].tag);
t.equal(actual.children[1].name, expected.name);
t.notEqual(actual.children[1], expected);
t.equal(actual.children[0], untouched);
});
});
test('toggleThisPanel', t => {
t.test('should update single node', t => {
const expected = { name: 'foo', isOpen: true };
const actual = toggleThisPanel(
{ children: [ { name: 'foo', isOpen: false }] },
'foo'
);
t.plan(1);
t.deepLooseEqual(actual.children[0], expected);
});
});
test('toggleAllPanels', t => {
t.test('should add `isOpen: true` to every node without children', t => {
const expected = {
isOpen: true,
children: [{
isOpen: true,
children: [{}, {}]
}]
};
const actual = expandAllPanels({ children: [{ children: [{}, {}] }] });
t.plan(1);
t.deepLooseEqual(actual, expected);
});
t.test('should add `isOpen: false` to every node without children', t => {
const leaf = {};
const expected = {
isOpen: false,
children: [{
isOpen: false,
children: [{}, leaf]
}]
};
const actual = collapseAllPanels(
{ isOpen: true, children: [{ children: [{}, leaf]}]},
);
t.plan(2);
t.deepLooseEqual(actual, expected);
t.equal(actual.children[0].children[1], leaf);
});
});
test('updatePath', t => {
t.test('should call update function for each node in the path', t => {
const expected = {
children: [
{
name: 'superFoo',
children: [
{
name: 'blockBar',
children: [{name: 'challBar'}]
},
{
name: 'blockFoo',
children: [{name: 'challFoo'}]
}
]
},
{
name: 'superBaz',
isOpen: false,
children: []
}
]
};
const spy = sinon.spy(t => ({ ...t}) );
spy.withArgs(expected.children[0]);
spy.withArgs(expected.children[0].children[1]);
spy.withArgs(expected.children[0].children[1].children[0]);
updatePath(expected, 'challFoo', spy);
t.plan(4);
t.equal(spy.callCount, 3);
t.ok(spy.withArgs(expected.children[0]).calledOnce, 'superBlock');
t.ok(spy.withArgs(expected.children[0].children[1]).calledOnce, 'block');
t.ok(
spy.withArgs(expected.children[0].children[1].children[0]).calledOnce,
'chall'
);
});
});
test('openPath', t=> {
t.test('should open all nodes in the path', t => {
const expected = {
children: [
{
name: 'superFoo',
isOpen: true,
children: [
{
name: 'blockBar',
isOpen: false,
children: []
},
{
name: 'blockFoo',
isOpen: true,
children: [{
name: 'challFoo'
}]
}
]
},
{
name: 'superBar',
isOpen: false,
children: []
}
]
};
const actual = openPath({
children: [
{
name: 'superFoo',
isOpen: false,
children: [
{
name: 'blockBar',
isOpen: false,
children: []
},
{
name: 'blockFoo',
isOpen: false,
children: [{
name: 'challFoo'
}]
}
]
},
{
name: 'superBar',
isOpen: false,
children: []
}
]
}, 'challFoo');
t.plan(1);
t.deepLooseEqual(actual, expected);
});
});

View File

@ -1,38 +0,0 @@
import React from 'react';
import Media from 'react-media';
import { Col, Navbar, Row } from 'react-bootstrap';
import FCCSearchBar from 'react-freecodecamp-search';
import { NavLogo, NavLinks } from './components';
import propTypes from './navPropTypes';
function LargeNav({ clickOnLogo }) {
return (
<Media
query='(min-width: 956px)'
render={
() => (
<Row>
<Col className='nav-component' sm={ 5 } xs={ 6 }>
<Navbar.Header>
<NavLogo clickOnLogo={ clickOnLogo } />
<FCCSearchBar />
</Navbar.Header>
</Col>
<Col className='nav-component bins' sm={ 3 } xs={ 6 }/>
<Col className='nav-component nav-links' sm={ 4 } xs={ 0 }>
<Navbar.Collapse>
<NavLinks />
</Navbar.Collapse>
</Col>
</Row>
)
}
/>
);
}
LargeNav.displayName = 'LargeNav';
LargeNav.propTypes = propTypes;
export default LargeNav;

View File

@ -1,42 +0,0 @@
import React from 'react';
import Media from 'react-media';
import { Navbar, Row } from 'react-bootstrap';
import FCCSearchBar from 'react-freecodecamp-search';
import { NavLogo, NavLinks } from './components';
import propTypes from './navPropTypes';
function MediumNav({ clickOnLogo }) {
return (
<Media
query={{ maxWidth: 955, minWidth: 751 }}
>
{
matches => matches && typeof window !== 'undefined' && (
<div>
<Row>
<Navbar.Header className='medium-nav'>
<div className='nav-component header'>
<Navbar.Toggle />
<NavLogo clickOnLogo={ clickOnLogo } />
<FCCSearchBar />
</div>
<div className='nav-component bins'/>
</Navbar.Header>
</Row>
<Row className='collapse-row'>
<Navbar.Collapse>
<NavLinks />
</Navbar.Collapse>
</Row>
</div>
)
}
</Media>
);
}
MediumNav.displayName = 'MediumNav';
MediumNav.propTypes = propTypes;
export default MediumNav;

View File

@ -1,60 +0,0 @@
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Navbar } from 'react-bootstrap';
import LargeNav from './LargeNav.jsx';
import MediumNav from './MediumNav.jsx';
import SmallNav from './SmallNav.jsx';
import {
clickOnLogo
} from './redux';
import propTypes from './navPropTypes';
const mapStateToProps = () => ({});
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
clickOnLogo
},
dispatch
);
}
const allNavs = [
LargeNav,
MediumNav,
SmallNav
];
function FCCNav(props) {
const {
clickOnLogo
} = props;
const withNavProps = Component => (
<Component
clickOnLogo={ clickOnLogo }
key={ Component.displayName }
/>
);
return (
<Navbar
className='nav-height'
id='navbar'
staticTop={ true }
>
{
allNavs.map(withNavProps)
}
</Navbar>
);
}
FCCNav.displayName = 'FCCNav';
FCCNav.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(FCCNav);

View File

@ -1,43 +0,0 @@
import React from 'react';
import Media from 'react-media';
import { Navbar, Row } from 'react-bootstrap';
import FCCSearchBar from 'react-freecodecamp-search';
import { NavLogo, NavLinks } from './components';
import propTypes from './navPropTypes';
function SmallNav({ clickOnLogo }) {
return (
<Media
query='(max-width: 750px)'
>
{
matches => matches && typeof window !== 'undefined' && (
<div>
<Row>
<Navbar.Header className='small-nav'>
<div className='nav-component header'>
<Navbar.Toggle />
<NavLogo clickOnLogo={ clickOnLogo } />
</div>
<div className='nav-component bins'/>
</Navbar.Header>
</Row>
<Row className='collapse-row'>
<Navbar.Collapse>
<NavLinks>
<FCCSearchBar />
</NavLinks>
</Navbar.Collapse>
</Row>
</div>
)
}
</Media>
);
}
SmallNav.displayName = 'SmallNav';
SmallNav.propTypes = propTypes;
export default SmallNav;

View File

@ -1,22 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'react-bootstrap';
const propTypes = {
content: PropTypes.string,
disabled: PropTypes.bool,
handleClick: PropTypes.func.isRequired
};
export default function BinButton({ content, handleClick, disabled }) {
return (
<Button
className={ disabled ? 'disabled-button' : 'enabled-button' }
onClick={ handleClick }
>
{ content }
</Button>
);
}
BinButton.displayName = 'BinButton';
BinButton.propTypes = propTypes;

View File

@ -1,35 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ButtonGroup } from 'react-bootstrap';
import BinButton from './Bin-Button.jsx';
const propTypes = {
panes: PropTypes.arrayOf(
PropTypes.shape({
actionCreator: PropTypes.func.isRequired,
content: PropTypes.string.isRequired
})
)
};
function BinButtons({ panes }) {
return (
<ButtonGroup>
{
panes.map(({ content, actionCreator, isHidden }) => (
<BinButton
content={ content }
disabled={ isHidden }
handleClick={ actionCreator }
key={ content }
/>
))
}
</ButtonGroup>
);
}
BinButtons.displayName = 'BinButtons';
BinButtons.propTypes = propTypes;
export default BinButtons;

View File

@ -1,151 +0,0 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { capitalize } from 'lodash';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { MenuItem, NavDropdown, NavItem, Nav } from 'react-bootstrap';
import navLinks from '../links.json';
import SignUp from './Sign-Up.jsx';
import { Link } from '../../Router';
import {
openDropdown,
closeDropdown,
dropdownSelector,
createNavLinkActionCreator
} from '../redux';
import { isSignedInSelector, signInLoadingSelector } from '../../redux';
const mapStateToProps = createSelector(
isSignedInSelector,
dropdownSelector,
signInLoadingSelector,
(isSignedIn, isDropdownOpen, showLoading) => ({
isDropdownOpen,
isSignedIn,
navLinks,
showLoading
})
);
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
...navLinks.reduce(
(mdtp, { content }) => {
const handler = `handle${capitalize(content)}Click`;
mdtp[handler] = createNavLinkActionCreator(content);
return mdtp;
}, {}),
closeDropdown,
openDropdown
},
dispatch
);
}
const navLinkPropType = PropTypes.shape({
content: PropTypes.string,
link: PropTypes.string,
isDropdown: PropTypes.bool,
target: PropTypes.string,
links: PropTypes.array
});
const propTypes = {
children: PropTypes.any,
closeDropdown: PropTypes.func.isRequired,
isDropdownOpen: PropTypes.bool,
isInNav: PropTypes.bool,
isSignedIn: PropTypes.bool,
navLinks: PropTypes.arrayOf(navLinkPropType),
openDropdown: PropTypes.func.isRequired,
showLoading: PropTypes.bool
};
class NavLinks extends PureComponent {
renderLink(isNavItem, { isReact, isDropdown, content, link, links, target }) {
const Component = isNavItem ? NavItem : MenuItem;
const {
isDropdownOpen,
openDropdown,
closeDropdown
} = this.props;
if (isDropdown) {
// adding a noop to NavDropdown to disable false warning
// about controlled component
return (
<NavDropdown
id={ `nav-${content}-dropdown` }
key={ content }
noCaret={ true }
onClick={ openDropdown }
onToggle={ isDropdownOpen ? closeDropdown : openDropdown }
open={ isDropdownOpen }
title={ content }
>
{ links.map(this.renderLink.bind(this, false)) }
</NavDropdown>
);
}
if (isReact) {
return (
<Link
key={ content }
onClick={ this.props[`handle${content}Click`] }
to={ link }
>
<Component
target={ target || null }
>
{ content }
</Component>
</Link>
);
}
return (
<Component
href={ link }
key={ content }
onClick={ this.props[`handle${content}Click`] }
target={ target || null }
>
{ content }
</Component>
);
}
render() {
const {
showLoading,
isSignedIn,
navLinks,
isInNav = true,
children
} = this.props;
return (
<Nav id='nav-links' navbar={ true } pullRight={ true }>
{ children }
{
navLinks.map(
this.renderLink.bind(this, isInNav)
)
}
<SignUp
isInDropDown={ !isInNav }
showLoading={ showLoading }
showSignUp={ !isSignedIn }
/>
</Nav>
);
}
}
NavLinks.displayName = 'NavLinks';
NavLinks.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(NavLinks);

View File

@ -1,45 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavbarBrand } from 'react-bootstrap';
import Media from 'react-media';
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
const fCCglyph = 'https://s3.amazonaws.com/freecodecamp/FFCFire.png';
const propTypes = {
clickOnLogo: PropTypes.func.isRequired
};
function NavLogo({ clickOnLogo }) {
return (
<NavbarBrand>
<a
href='/'
onClick={ clickOnLogo }
>
<Media query='(min-width: 735px)'>
{
matches => matches ? (
<img
alt='learn to code javascript at freeCodeCamp logo'
className='nav-logo logo'
src={ fCClogo }
/>
) : (
<img
alt='learn to code javascript at freeCodeCamp logo'
className='nav-logo logo'
src={ fCCglyph }
/>
)
}
</Media>
</a>
</NavbarBrand>
);
}
NavLogo.displayName = 'NavLogo';
NavLogo.propTypes = propTypes;
export default NavLogo;

View File

@ -1,50 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { MenuItem, NavItem } from 'react-bootstrap';
import { Link } from '../../Router';
import { onRouteSettings } from '../../routes/Settings/redux';
const propTypes = {
isInDropDown: PropTypes.bool,
showLoading: PropTypes.bool,
showSignUp: PropTypes.bool
};
function SignUpButton({ isInDropDown, showLoading, showSignUp }) {
if (showLoading) {
return null;
}
if (showSignUp) {
return isInDropDown ? (
<MenuItem
href='/signup'
key='signup'
>
Sign Up
</MenuItem>
) : (
<NavItem
href='/signup'
key='signup'
>
Sign Up
</NavItem>
);
}
return (
<li
className='nav-avatar'
key='user'
>
<Link to={ onRouteSettings() }>
Settings
</Link>
</li>
);
}
SignUpButton.displayName = 'SignUpButton';
SignUpButton.propTypes = propTypes;
export default SignUpButton;

View File

@ -1,3 +0,0 @@
export { default as BinButtons } from './BinButtons.jsx';
export { default as NavLogo } from './NavLogo.jsx';
export { default as NavLinks } from './NavLinks.jsx';

View File

@ -1 +0,0 @@
export default from './Nav.jsx';

View File

@ -1,15 +0,0 @@
[
{
"content": "Curriculum",
"link": "https://learn.freecodecamp.org"
},
{
"content": "Forum",
"link": "https://forum.freecodecamp.org/",
"target": "_blank"
},
{
"content": "News",
"link": "https://www.freecodecamp.org/news"
}
]

View File

@ -1,345 +0,0 @@
.navbar {
background-color: @brand-primary;
font-size: 14px;
}
.navbar-nav > li > a {
color: @body-bg;
&:hover {
color: @brand-primary;
}
}
.navbar > .container {
padding-right: 0px;
width: auto;
@media (max-width: 767px) {
// container default padding size
padding-left: 15px;
padding-right: 15px;
}
}
.nav-height {
border: none;
height: @navbar-height;
width: 100%;
}
@navbar-logo-height: 25px;
@navbar-logo-padding: (@navbar-height - @navbar-logo-height) / 2;
.navbar-brand {
padding-top: @navbar-logo-padding;
padding-bottom: @navbar-logo-padding;
display: flex;
align-items: center;
justify-content: center;
}
.nav-logo {
height: @navbar-logo-height;
}
.navbar-right {
background-color: @brand-primary;
text-align: center;
@media (min-width: @screen-md-min) {
margin-right:0;
}
@media (min-width: @screen-md-max) and (max-width: 991px) {
left: 0;
margin-right: 0;
position: absolute;
right: 0;
white-space: nowrap;
}
}
.navbar {
white-space: nowrap;
border: none;
line-height: 1;
@media (min-width: 767px) {
padding-left: 15px;
padding-right: 30px;
}
}
// li is used here to get more specific
// and win against navbar.less#273
li.nav-avatar {
span {
display: inline-block;
}
@media (min-width: @screen-sm-min) {
height: @navbar-height;
margin: 0;
padding: 0;
> a {
margin: 0;
padding: 7.5px @navbar-padding-horizontal 7.5px @navbar-padding-horizontal;
}
}
}
.navbar-nav a {
color: @body-bg;
margin-top: -5px;
margin-bottom: -5px;
}
.navbar-toggle {
color: @body-bg;
&:hover,
&:focus {
color: #4a2b0f;
}
}
.navbar-collapse {
border-top: 0;
}
.divider-vertical {
height: 24px;
margin-top: 6px;
margin-bottom: 6px;
border-left: 0.25px solid #ffffff;
border-right: 0.25px solid #ffffff;
}
@media (max-width: @screen-xs-max) {
.navbar-header {
float: none;
}
.navbar-toggle {
display: block;
}
.navbar-collapse.collapse {
display: none !important;
}
.navbar-nav {
margin-top: 0;
}
.navbar-nav > li {
float: none;
}
.navbar-nav > li > a {
padding-top: 10px;
padding-bottom: 10px;
}
.navbar-text {
float: none;
margin: 15px 0;
}
/* since 3.1.0 */
.navbar-collapse.collapse.in {
display: block !important;
}
.collapsing {
overflow: hidden !important;
position: absolute;
left: 0;
right: 0;
}
}
.night {
.nav-component-wrapper {
::-webkit-input-placeholder {
color: @night-text-color;
}
::-moz-placeholder {
color: @night-text-color;
}
::-ms-placeholder {
color: @night-text-color;
}
::placeholder {
color: @night-text-color;
}
.fcc_input {
background-color: @night-search-color;
color: @night-text-color;
}
}
.navbar-default {
.navbar-nav {
& > li > a {
color: #CCC;
}
.dropdown-menu {
background-color: @gray;
a {
color: @night-text-color !important;
}
}
a:focus,
a:hover,
.open #nav-Community-dropdown {
background-color: #666 !important;
color: @link-hover-color !important;
}
}
}
.navbar-toggle {
&:hover,
&:focus {
background-color: #666;
color: @link-hover-color;
border-color: #666;
}
}
}
@media (min-width: 768px) and (max-width: 860px) {
.navbar {
padding-left: 0;
padding-right: 0;
}
.navbar-right {
margin-right: 0;
}
.navbar-nav > li > a {
padding-left: 10px;
padding-right: 10px;
}
}
.nav-component {
display: flex;
align-items: center;
&.header{
.navbar-brand {
padding-left: 0px;
}
}
&.bins {
justify-content: center;
.nav {
display: flex;
}
}
&.nav-links {
justify-content: flex-end;
}
.fcc_searchBar {
width: auto;
flex-grow: 1;
::-webkit-input-placeholder {
color: @input-color-placeholder;
}
::-moz-placeholder {
color: @input-color-placeholder;
}
::-ms-placeholder {
color: @input-color-placeholder;
}
::placeholder {
color: @input-color-placeholder;
}
.ais-Hits {
background-color: white;
z-index: 5;
}
}
.navbar-header {
display: flex;
align-items: center;
width: 100%;
}
}
.medium-nav {
display: flex;
justify-content: space-between;
.bins {
justify-content: flex-end;
}
}
.small-nav {
display: flex;
justify-content: space-between;
.bins {
justify-content: flex-end;
.btn {
padding: 6px 4px;
}
}
}
.bins {
.disabled-button {
color: whitesmoke;
}
.enabled-button {
color: black;
}
.btn {
border-color: @brand-primary;
background-color: white;
&:hover {
background-color: @brand-primary;
}
}
}
.collapse-row {
background-color: @brand-primary;
.dropdown-menu {
li a {
display: flex;
justify-content: center;
background-color: #eeeeee;
color: @brand-primary !important;
&:hover {
background-color: @brand-primary !important;
color: #eeeeee !important;
}
}
}
}

View File

@ -1,5 +0,0 @@
import PropTypes from 'prop-types';
export default {
clickOnLogo: PropTypes.func.isRequired
};

View File

@ -1 +0,0 @@
"nav"

View File

@ -1,76 +0,0 @@
import capitalize from 'lodash/capitalize';
import noop from 'lodash/noop';
import {
createAction,
createTypes,
handleActions
} from 'berkeleys-redux-utils';
import ns from '../ns.json';
import { createEventMetaCreator } from '../../analytics/index';
export const epics = [];
export const types = createTypes([
'clickOnLogo',
'clickOnMap',
'navLinkClicked',
'closeDropdown',
'openDropdown'
], ns);
export const clickOnLogo = createAction(
types.clickOnLogo,
noop,
createEventMetaCreator({
category: 'Nav',
action: 'clicked',
label: 'fcc logo clicked'
})
);
export const clickOnMap = createAction(
types.clickOnMap,
noop,
createEventMetaCreator({
category: 'Nav',
action: 'clicked',
label: 'map button clicked'
})
);
export const closeDropdown = createAction(types.closeDropdown);
export const openDropdown = createAction(types.openDropdown);
export function createNavLinkActionCreator(link) {
return createAction(
types.navLinkClicked,
noop,
createEventMetaCreator({
category: capitalize(ns),
action: 'click',
label: `${link} link`
})
);
}
const initialState = {
isDropdownOpen: false
};
export const dropdownSelector = state => state[ns].isDropdownOpen;
export default handleActions(
() => ({
[types.closeDropdown]: state => ({
...state,
isDropdownOpen: false
}),
[types.openDropdown]: state => ({
...state,
isDropdownOpen: true
})
}),
initialState,
ns
);

View File

@ -1,31 +0,0 @@
import React from 'react';
// import PropTypes from 'prop-types';
import {
Alert,
Button
} from 'react-bootstrap';
const propTypes = {};
export default function NotFound() {
return (
<div className='not-found'>
<Alert bsStyle='info'>
<p>
{ 'Sorry, we couldn\'t find a page for that address.' }
</p>
</Alert>
<a href={'/map'}>
<Button
bsSize='lg'
bsStyle='primary'
>
Take me to the Challenges
</Button>
</a>
</div>
);
}
NotFound.displayName = 'NotFound';
NotFound.propTypes = propTypes;

View File

@ -1 +0,0 @@
export default from './Not-Found.jsx';

View File

@ -1,85 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import toUrl from './to-url.js';
import createHandler from './handle-press.js';
import { routesMapSelector } from './redux';
const mapStateToProps = createSelector(
routesMapSelector,
routesMap => ({ routesMap })
);
const propTypes = {
children: PropTypes.node,
dispatch: PropTypes.func,
onClick: PropTypes.func,
redirect: PropTypes.bool,
replace: PropTypes.bool,
routesMap: PropTypes.object,
shouldDispatch: PropTypes.bool,
style: PropTypes.object,
target: PropTypes.string,
to: PropTypes.oneOfType([ PropTypes.object, PropTypes.string ]).isRequired
};
export const Link = (
{
children,
dispatch,
onClick,
redirect,
replace,
routesMap,
shouldDispatch = true,
style,
target,
to
}
) => {
const url = toUrl(to, routesMap);
const handler = createHandler(
url,
routesMap,
onClick,
shouldDispatch,
target,
dispatch,
to,
replace || redirect
);
const localProps = {};
if (url) {
localProps.href = url;
}
if (handler) {
localProps.onMouseDown = handler;
localProps.onTouchStart = handler;
}
if (target) {
localProps.target = target;
}
return (
<a
onClick={ handler }
style={ style }
{ ...localProps }
>
{children}
</a>
);
};
Link.contextTypes = {
store: PropTypes.object.isRequired
};
Link.propTypes = propTypes;
export default connect(mapStateToProps)(Link);

View File

@ -1,50 +0,0 @@
import { pathToAction, redirect, getOptions } from 'redux-first-router';
const isAction = to => typeof to === 'object' &&
!Array.isArray(to);
const isModified = e => !!(
e.metaKey ||
e.altKey ||
e.ctrlKey ||
e.shiftKey
);
export default (
url,
routesMap,
onClick,
shouldDispatch,
target,
dispatch,
to,
dispatchRedirect
) => e => {
let shouldGo = true;
if (onClick) {
// onClick can return false to prevent dispatch
shouldGo = onClick(e);
shouldGo = typeof shouldGo === 'undefined' ? true : shouldGo;
}
const prevented = e.defaultPrevented;
if (!target && e && e.preventDefault && !isModified(e)) {
e.preventDefault();
}
if (
shouldGo &&
shouldDispatch &&
!target &&
!prevented &&
e.button === 0 &&
!isModified(e)
) {
const { querySerializer: serializer } = getOptions();
let action = isAction(to) ? to : pathToAction(url, routesMap, serializer);
action = dispatchRedirect ? redirect(action) : action;
dispatch(action);
}
};

View File

@ -1 +0,0 @@
export { default as Link } from './Link.jsx';

View File

@ -1,8 +0,0 @@
import { selectLocationState } from 'redux-first-router';
export const paramsSelector = state => selectLocationState(state).payload || {};
export const locationTypeSelector =
state => selectLocationState(state).type || '';
export const routesMapSelector = state =>
selectLocationState(state).routesMap || {};
export const pathnameSelector = state => selectLocationState(state).pathname;

View File

@ -1,41 +0,0 @@
import { actionToPath, getOptions } from 'redux-first-router';
export default (to, routesMap) => {
if (to && typeof to === 'string') {
return to;
}
if (typeof to === 'object') {
const { payload = {}, ...action } = to;
try {
const { querySerializer } = getOptions();
return actionToPath(
{
...action,
payload: {
...payload
}
},
routesMap,
querySerializer
);
} catch (e) {
console.error(e);
console.warn(
'[Link] could not create path from action:',
action,
'For reference, here are your current routes:',
routesMap
);
return '#';
}
}
console.warn(
'[Link] `to` prop must be a string or action object. You provided: ',
to
);
return '#';
};

View File

@ -1,91 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { NotificationStack } from '@freecodecamp/react-notification';
import { removeToast } from './redux';
const registeredActions = {};
const mapStateToProps = state => ({ toasts: state.toasts });
// we use styles here to overwrite those built into the library
// but there are some styles applied using
// regular css in /client/less/toastr.less
const barStyle = {
fontSize: '2rem',
// null values let our css set the style prop
padding: null
};
const rightBarStyle = {
...barStyle,
left: null,
right: '-100%'
};
const actionStyle = {
fontSize: '2rem'
};
const addDispatchableActionsToToast = createSelector(
state => state.toasts,
state => state.dispatch,
(toasts, dispatch) => toasts.map(({ position, actionCreator, ...toast }) => {
const activeBarStyle = {};
let finalBarStyle = barStyle;
if (position !== 'left') {
activeBarStyle.left = null;
activeBarStyle.right = '1rem';
finalBarStyle = rightBarStyle;
}
const onClick = !registeredActions[actionCreator] ?
() => {
dispatch(removeToast(toast));
} :
() => {
dispatch(registeredActions[actionCreator]());
dispatch(removeToast(toast));
};
return {
...toast,
barStyle: finalBarStyle,
activeBarStyle,
actionStyle,
onClick
};
})
);
const propTypes = {
dispatch: PropTypes.func,
toasts: PropTypes.arrayOf(PropTypes.object)
};
export class Toasts extends React.Component {
constructor(...props) {
super(...props);
this.handleDismiss = this.handleDismiss.bind(this);
}
styleFactory(index, style) {
return { ...style, bottom: `${4 + index * 8}rem` };
}
handleDismiss(notification) {
this.props.dispatch(removeToast(notification));
}
render() {
const { toasts = [], dispatch } = this.props;
return (
<NotificationStack
activeBarStyle={ this.styleFactory }
barStyle={ this.styleFactory }
notifications={
addDispatchableActionsToToast({ toasts, dispatch })
}
onDismiss={ this.handleDismiss }
/>
);
}
}
Toasts.displayName = 'Toasts';
Toasts.propTypes = propTypes;
export default connect(mapStateToProps)(Toasts);

View File

@ -1 +0,0 @@
export default from './Toasts.jsx';

View File

@ -1 +0,0 @@
"toasts"

View File

@ -1,47 +0,0 @@
import {
createAction,
createTypes,
handleActions
} from 'berkeleys-redux-utils';
import ns from '../ns.json';
export const types = createTypes([
'makeToast',
'removeToast'
], ns);
let key = 0;
export const makeToast = createAction(
types.makeToast,
({ timeout, ...rest }) => ({
...rest,
// assign current value of key to new toast
// and then increment key value
key: key++,
dismissAfter: timeout || 6000,
position: rest.position === 'left' ? 'left' : 'right'
})
);
export const removeToast = createAction(
types.removeToast,
({ key }) => key
);
const initialState = [];
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
)
}),
initialState,
ns
);

View File

@ -1,34 +0,0 @@
const throwIfUndefined = () => {
throw new TypeError('Argument must not be of type `undefined`');
};
// createEventMetaCreator({
// category: String,
// action: String,
// label?: String,
// value?: Number
// }) => () => Object
export const createEventMetaCreator = ({
// categories are features or namespaces of the app (capitalized):
// Map, Nav, Challenges, and so on
category = throwIfUndefined,
// can be a one word the event
// click, play, toggle.
// This is not a hard and fast rule
action = throwIfUndefined,
// any additional information
// when in doubt use redux action type
// or a short sentence describing the
// action
label,
// used to tack some specific value for a GA event
value
} = throwIfUndefined) => () => ({
analytics: {
type: 'event',
category,
action,
label,
value
}
});

View File

@ -1,26 +0,0 @@
// should match ./ns.json value and filename
@ns: app;
.@{ns}-container {
// we invert the nav and content since
// the content needs to render first
// Here we invert the order in which
// they are painted using css so the
// nav is on top again
.grid(@direction: column; @wrap: nowrap);
height: 100%;
width: 100%;
}
.@{ns}-content {
// makes the initial content height 0px
// then lets it grow to fit the rest of the space
flex: 1 0 0px;
// allow content to adapt to screen size
display: flex;
}
.@{ns}-centered {
.center(@value: @container-xl, @padding: @grid-gutter-width);
margin-top: @navbar-margin-bottom;
}

View File

@ -1,104 +0,0 @@
import { Observable } from 'rx';
import createDebugger from 'debug';
import { compose, createStore, applyMiddleware } from 'redux';
import { selectLocationState, connectRoutes } from 'redux-first-router';
import { combineReducers } from 'berkeleys-redux-utils';
import { createEpic } from 'redux-epic';
import appReducer from './reducer.js';
import routesMap from './routes-map.js';
import epics from './epics';
import servicesCreator from '../utils/services-creator';
const debug = createDebugger('fcc:app:createApp');
// createApp(settings: {
// history?: History,
// defaultState?: Object|Void,
// serviceOptions?: Object,
// middlewares?: [...Function],
// enhancers?: [...Function],
// epics?: [...Function],
// }) => Observable
//
// Either location or history must be defined
export default function createApp({
history,
defaultState,
serviceOptions = {},
middlewares: sideMiddlewares = [],
enhancers: sideEnhancers = [],
epics: sideEpics = [],
epicOptions: sideEpicOptions = {}
}) {
const epicOptions = {
...sideEpicOptions,
services: servicesCreator(serviceOptions)
};
const epicMiddleware = createEpic(
epicOptions,
...epics,
...sideEpics
);
const {
reducer: routesReducer,
middleware: routesMiddleware,
enhancer: routesEnhancer
} = connectRoutes(history, routesMap);
routesReducer.toString = () => 'location';
const enhancer = compose(
routesEnhancer,
applyMiddleware(
routesMiddleware,
epicMiddleware,
...sideMiddlewares
),
// enhancers must come after middlewares
// on client side these are things like Redux DevTools
...sideEnhancers
);
const reducer = combineReducers(
appReducer,
routesReducer
);
// create composed store enhancer
// use store enhancer function to enhance `createStore` function
// call enhanced createStore function with reducer and defaultState
// to create store
const store = createStore(reducer, defaultState, enhancer);
const location = selectLocationState(store.getState());
// note(berks): should get stripped in production client by webpack
// We need to find a way to hoist to top level in production node env
// babel plugin, maybe? After a quick search I couldn't find one
if (process.env.NODE_ENV === 'development') {
if (module.hot) {
module.hot.accept('./reducer.js', () => {
debug('hot reloading reducers');
store.replaceReducer(combineReducers(
require('./reducer.js').default,
routesReducer
));
});
}
}
// ({
// redirect,
// props,
// reducer,
// store,
// epic: epicMiddleware
// }));
return Observable.of({
store,
epic: epicMiddleware,
location,
notFound: false
});
}

View File

@ -1,331 +0,0 @@
import { findIndex, property, merge } from 'lodash';
import uuid from 'uuid/v4';
import {
composeReducers,
createAction,
createTypes,
handleActions
} from 'berkeleys-redux-utils';
import { themes } from '../../utils/themes';
import { usernameSelector } from '../redux';
import { types as map } from '../Map/redux';
import legacyProjects from '../../utils/legacyProjectData';
export const ns = 'entities';
export const getNS = state => state[ns];
export const entitiesSelector = getNS;
export const types = createTypes([
'addPortfolioItem',
'optoUpdatePortfolio',
'regresPortfolio',
'resetFullBlocks',
'updateLocalProfileUI',
'updateMultipleUserFlags',
'updateTheme',
'updateUserFlag',
'updateUserEmail',
'updateUserLang',
'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,
(username, flag) => ({ username, flag })
);
// updateUserEmail(username: String, email: String) => Action
export const updateUserEmail = createAction(
types.updateUserEmail,
(username, email) => ({ username, email })
);
// updateUserLang(username: String, lang: String) => Action
export const updateUserLang = createAction(
types.updateUserLang,
(username, lang) => ({ username, languageTag: lang })
);
export const updateLocalProfileUI = createAction(types.updateLocalProfileUI);
export const resetFullBlocks = createAction(types.resetFullBlocks);
export const updateUserCurrentChallenge = createAction(
types.updateUserCurrentChallenge
);
// entity meta creators
const getEntityAction = property('meta.entitiesAction');
export const updateThemeMetacreator = (username, theme) => ({
entitiesAction: {
type: types.updateTheme,
payload: {
username,
theme: !theme || theme === themes.default ? themes.default : themes.night
}
}
});
export function emptyPortfolio() {
return {
id: uuid(),
title: '',
description: '',
url: '',
image: ''
};
}
const defaultState = {
superBlock: {},
block: {},
challenge: {},
user: {},
fullBlocks: []
};
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 {
block: blocks,
challenge: challengeMap
} = getNS(state);
const idToNameMap = challengeIdToNameMapSelector(state);
const legacyWithDashedNames = legacyProjects
.reduce((list, current) => ([
...list,
{
...current,
challenges: current.challenges.map(id => idToNameMap[id])
}
]),
[]
);
blocks['full-stack-projects'] = {
dashedName: 'full-stack',
title: 'Full Stack Certification',
time: '1800 hours',
challenges: [],
superBlock: 'full-stack'
};
return Object.keys(blocks)
.filter(key =>
key.includes('projects') && !(
key.includes('coding-interview') || key.includes('take-home')
)
)
.map(key => blocks[key])
.concat(legacyWithDashedNames)
.map(({ title, challenges, superBlock }) => {
const projectChallengeDashNames = challenges
// challengeIdToName is not available on appMount
.filter(Boolean)
// remove any project intros
.filter(chal => !chal.includes('get-set-for'));
const projectChallenges = projectChallengeDashNames
.map(dashedName => {
const { id, title } = challengeMap[dashedName];
return { id, title, dashedName };
});
return {
projectBlockName: title,
superBlock,
challenges: projectChallenges
};
});
}
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 || {};
return superBlock[name] || {};
};
}
export const isChallengeLoaded = (state, { dashedName }) =>
!!challengeMapSelector(state)[dashedName];
export const fullBlocksSelector = state => getNS(state).fullBlocks;
export default composeReducers(
ns,
function metaReducer(state = defaultState, action) {
const { meta } = action;
if (meta && meta.entities) {
if (meta.entities.user) {
return {
...state,
user: {
...state.user,
...meta.entities.user
}
};
}
return merge({}, state, action.meta.entities);
}
return state;
},
function entitiesReducer(state = defaultState, action) {
if (getEntityAction(action)) {
const { payload: { username, theme } } = getEntityAction(action);
return {
...state,
user: {
...state.user,
[username]: {
...state.user[username],
theme
}
}
};
}
return state;
},
handleActions(
() => ({
[map.fetchMapUi.complete]: (state, { payload: { entities } }) =>
merge({}, state, entities),
[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: {
...state.user,
[username]: {
...state.user[username],
[flag]: !state.user[username][flag]
}
}
}),
[types.updateUserEmail]: (state, { payload: { username, email } }) => ({
...state,
user: {
...state.user,
[username]: {
...state.user[username],
email
}
}
}),
[types.updateUserLang]:
(
state,
{
payload: { username, languageTag }
}
) => ({
...state,
user: {
...state.user,
[username]: {
...state.user[username],
languageTag
}
}
}),
[types.updateLocalProfileUI]:
(
state,
{ payload: { username, profileUI } }
) => ({
...state,
user: {
...state.user,
[username]: {
...state.user[username],
profileUI: {
...state.user[username].profileUI,
...profileUI
}
}
}
})
}),
defaultState
)
);

View File

@ -1,13 +0,0 @@
import { epics as app } from './redux';
import { epics as flash } from './Flash/redux';
import { epics as map } from './Map/redux';
import { epics as nav } from './Nav/redux';
import { epics as settings } from './routes/Settings/redux';
export default [
...app,
...flash,
...map,
...nav,
...settings
];

View File

@ -1,3 +0,0 @@
export { default as createApp } from './create-app.jsx';
export { default as App } from './App.jsx';
export { default as provideStore } from './provide-store.js';

View File

@ -1,4 +0,0 @@
&{ @import "./app.less"; }
&{ @import "./Nav/nav.less"; }
&{ @import "./Flash/flash.less"; }
&{ @import "./routes/index.less"; }

View File

@ -1 +0,0 @@
"app"

View File

@ -1,11 +0,0 @@
/* eslint-disable react/display-name */
import React from 'react';
import { Provider } from 'react-redux';
export default function provideStore(Component, store) {
return (
<Provider store={ store } >
<Component />
</Provider>
);
}

View File

@ -1,23 +0,0 @@
import { combineReducers } from 'berkeleys-redux-utils';
import app from './redux';
import entities from './entities';
import { reducer as form } from 'redux-form';
import map from './Map/redux';
import nav from './Nav/redux';
import routes from './routes/redux';
import toasts from './Toasts/redux';
import flash from './Flash/redux';
form.toString = () => 'form';
export default combineReducers(
app,
entities,
map,
nav,
routes,
toasts,
flash,
form
);

View File

@ -1,42 +0,0 @@
import { Observable } from 'rx';
import { ofType, combineEpics } from 'redux-epic';
import { getJSON$ } from '../../utils/ajax-stream';
import {
types,
fetchUserComplete,
fetchOtherUserComplete,
createErrorObservable,
showSignIn
} from './';
import { userFound } from '../routes/Profile/redux';
function getUserEpic(actions, _, { services }) {
return actions::ofType('' + types.fetchUser)
.flatMap(() => {
return services.readService$({ service: 'user' })
.filter(({ entities, result }) => entities && !!result)
.map(fetchUserComplete)
.defaultIfEmpty(showSignIn())
.catch(createErrorObservable);
});
}
function getOtherUserEpic(actions$) {
return actions$::ofType(types.fetchOtherUser.start)
.distinctUntilChanged()
.flatMap(({ payload: otherUser }) => {
return getJSON$(`/api/users/get-public-profile?username=${otherUser}`)
.flatMap(response => Observable.of(
fetchOtherUserComplete(response),
userFound(!!response.result)
))
.catch(createErrorObservable);
});
}
export default combineEpics(
getUserEpic,
getOtherUserEpic
);

View File

@ -1,220 +0,0 @@
import { flow, identity } from 'lodash';
import { Observable } from 'rx';
import {
combineActions,
createAction,
createAsyncTypes,
createTypes,
handleActions
} from 'berkeleys-redux-utils';
import { createSelector } from 'reselect';
import fetchUserEpic from './fetch-user-epic.js';
import nightModeEpic from './night-mode-epic.js';
import {
updateThemeMetacreator,
entitiesSelector
} from '../entities';
import { utils } from '../Flash/redux';
import { paramsSelector } from '../Router/redux';
import { types as map } from '../Map/redux';
import ns from '../ns.json';
import { themes, invertTheme } from '../../utils/themes.js';
export const epics = [
fetchUserEpic,
nightModeEpic
];
export const types = createTypes([
'onRouteHome',
'appMounted',
'analytics',
'updateTitle',
createAsyncTypes('fetchOtherUser'),
createAsyncTypes('fetchUser'),
'showSignIn',
'handleError',
// used to hit the server
'hardGoTo',
'delayedRedirect',
// night mode
'toggleNightMode',
createAsyncTypes('postTheme')
], ns);
const throwIfUndefined = () => {
throw new TypeError('Argument must not be of type `undefined`');
};
// createEventMetaCreator({
// category: String,
// action: String,
// label?: String,
// value?: Number
// }) => () => Object
export function createEventMetaCreator({
// categories are features or namespaces of the app (capitalized):
// Map, Nav, Challenges, and so on
category = throwIfUndefined,
// can be a one word the event
// click, play, toggle.
// This is not a hard and fast rule
action = throwIfUndefined,
// any additional information
// when in doubt use redux action type
// or a short sentence describing the action
label,
// used to tack some specific value for a GA event
value
} = throwIfUndefined) {
return () => ({
analytics: {
type: 'event',
category,
action,
label,
value
}
});
}
export const onRouteHome = createAction(types.onRouteHome);
export const appMounted = createAction(types.appMounted);
// updateTitle(title: String) => Action
export const updateTitle = createAction(types.updateTitle);
// fetchOtherUser() => Action
// used in combination with fetch-user-epic
// to fetch another users profile
export const fetchOtherUser = createAction(types.fetchOtherUser.start);
export const fetchOtherUserComplete = createAction(
types.fetchOtherUser.complete,
({ result }) => result,
identity
);
// fetchUser() => Action
// used in combination with fetch-user-epic
export const fetchUser = createAction(types.fetchUser);
export const fetchUserComplete = createAction(
types.fetchUser.complete,
({ result }) => result,
identity
);
export const showSignIn = createAction(types.showSignIn);
// used when server needs client to redirect
export const delayedRedirect = createAction(types.delayedRedirect);
// hardGoTo(path: String) => Action
export const hardGoTo = createAction(types.hardGoTo);
export const createErrorObservable = error => Observable.just({
type: types.handleError,
error
});
// use sparingly
// doActionOnError(
// actionCreator: (() => Action|Null)
// ) => (error: Error) => Observable[Action]
export const doActionOnError = actionCreator => error => Observable.of(
{
type: types.handleError,
error
},
actionCreator()
);
export const toggleNightMode = createAction(
types.toggleNightMode,
null,
(username, theme) => updateThemeMetacreator(username, invertTheme(theme))
);
export const postThemeComplete = createAction(
types.postTheme.complete,
null,
utils.createFlashMetaAction
);
export const postThemeError = createAction(
types.postTheme.error,
null,
(username, theme, err) => ({
...updateThemeMetacreator(username, invertTheme(theme)),
...utils.createFlashMetaAction(err)
})
);
const defaultState = {
title: 'Learn To Code | freeCodeCamp',
isSignInAttempted: false,
user: '',
csrfToken: '',
superBlocks: []
};
export const getNS = state => state[ns];
export const csrfSelector = state => getNS(state).csrfToken;
export const titleSelector = state => getNS(state).title;
export const signInLoadingSelector = state => !getNS(state).isSignInAttempted;
export const usernameSelector = state => getNS(state).user || '';
export const userSelector = createSelector(
state => getNS(state).user,
state => entitiesSelector(state).user,
(username, userMap) => userMap[username] || {}
);
export const userByNameSelector = state => {
const username = paramsSelector(state).username;
const userMap = entitiesSelector(state).user;
return userMap[username] || {};
};
export const themeSelector = flow(
userSelector,
user => user.theme || themes.default
);
export const isSignedInSelector = state => !!userSelector(state).username;
export default handleActions(
() => ({
[types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
...state,
title: payload + ' | freeCodeCamp'
}),
[types.fetchUser.complete]: (state, { payload: user }) => ({
...state,
user
}),
[map.fetchMapUi.complete]: (state, { payload }) => ({
...state,
superBlocks: payload.result.superBlocks
}),
[
combineActions(types.showSignIn, types.fetchUser.complete)
]: state => ({
...state,
isSignInAttempted: true
}),
[types.delayedRedirect]: (state, { payload }) => ({
...state,
delayedRedirect: payload
})
}),
defaultState,
ns
);

View File

@ -1,64 +0,0 @@
import _ from 'lodash';
import { Observable } from 'rx';
import { ofType } from 'redux-epic';
import store from '@freecodecamp/store';
import { themes } from '../../utils/themes.js';
import { postJSON$ } from '../../utils/ajax-stream.js';
import {
csrfSelector,
postThemeComplete,
postThemeError,
themeSelector,
types,
usernameSelector
} from './index.js';
function persistTheme(theme) {
store.set('fcc-theme', theme);
}
export default function nightModeEpic(
actions,
{ getState },
{ document }
) {
return Observable.of(document)
// if document is undefined we do nothing (ssr trap)
.filter(Boolean)
.flatMap(({ body }) => {
const toggleBodyClass = actions
::ofType(
types.fetchUser.complete,
types.toggleNightMode,
types.postTheme.complete,
types.postTheme.error
)
.map(_.flow(getState, themeSelector))
// catch existing night mode users
.do(persistTheme)
.do(theme => {
if (theme === themes.night) {
body.classList.add(themes.night);
} else {
body.classList.remove(themes.night);
}
})
.ignoreElements();
const postThemeEpic = actions::ofType(types.toggleNightMode)
.debounce(250)
.flatMapLatest(() => {
const _csrf = csrfSelector(getState());
const theme = themeSelector(getState());
const username = usernameSelector(getState());
return postJSON$('/update-my-theme', { _csrf, theme })
.map(postThemeComplete)
.catch(err => {
return Observable.of(postThemeError(username, theme, err));
});
});
return Observable.merge(toggleBodyClass, postThemeEpic);
});
}

View File

@ -1,9 +0,0 @@
import flowRight from 'lodash/flowRight';
import { createNameIdMap } from '../../utils/map.js';
export const shapeChallenges = flowRight(
entities => ({
...entities,
...createNameIdMap(entities)
})
);

View File

@ -1,17 +0,0 @@
import reduce from 'lodash/reduce';
import { types } from './redux';
import routes from './routes';
export default {
...reduce(routes, (routes, route, type) => {
let newRoute;
if (typeof route === 'string') {
newRoute = route;
} else {
newRoute = { ...route, path: route.path };
}
routes[type] = newRoute;
return routes;
}, {}),
[types.routeOnHome]: '/'
};

View File

@ -1 +0,0 @@
in case we ever want an admin panel

View File

@ -1,267 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import {
Alert,
Button,
Grid
} from 'react-bootstrap';
import {
updateTitle,
isSignedInSelector,
signInLoadingSelector,
usernameSelector,
userByNameSelector,
fetchOtherUser
} from '../../redux';
import { userFoundSelector } from './redux';
import { paramsSelector } from '../../Router/redux';
import ns from './ns.json';
import ChildContainer from '../../Child-Container.jsx';
import { Link } from '../../Router';
import CamperHOC from './components/CamperHOC.jsx';
import Portfolio from './components/Portfolio.jsx';
import Certificates from './components/Certificates.jsx';
import Timeline from './components/Timeline.jsx';
import HeatMap from './components/HeatMap.jsx';
import { FullWidthRow, Loader } from '../../helperComponents';
const mapStateToProps = createSelector(
isSignedInSelector,
userByNameSelector,
paramsSelector,
usernameSelector,
signInLoadingSelector,
userFoundSelector,
(
isSignedIn,
{
username: requestedUsername,
profileUI: {
isLocked,
showAbout,
showCerts,
showHeatMap,
showLocation,
showName,
showPoints,
showPortfolio,
showTimeLine
} = {}
},
{ username: paramsUsername },
currentUsername,
showLoading,
isUserFound
) => ({
isSignedIn,
currentUsername,
isCurrentUserProfile: paramsUsername === currentUsername,
isUserFound,
fetchOtherUserCompleted: typeof isUserFound === 'boolean',
paramsUsername,
requestedUsername,
isLocked,
showLoading,
showAbout,
showCerts,
showHeatMap,
showLocation,
showName,
showPoints,
showPortfolio,
showTimeLine
})
);
const mapDispatchToProps = {
fetchOtherUser,
updateTitle
};
const propTypes = {
currentUsername: PropTypes.string,
fetchOtherUser: PropTypes.func.isRequired,
fetchOtherUserCompleted: PropTypes.bool,
isCurrentUserProfile: PropTypes.bool,
isLocked: PropTypes.bool,
isSignedIn: PropTypes.bool,
isUserFound: PropTypes.bool,
paramsUsername: PropTypes.string,
requestedUsername: PropTypes.string,
showAbout: PropTypes.bool,
showCerts: PropTypes.bool,
showHeatMap: PropTypes.bool,
showLoading: PropTypes.bool,
showLocation: PropTypes.bool,
showName: PropTypes.bool,
showPoints: PropTypes.bool,
showPortfolio: PropTypes.bool,
showTimeLine: PropTypes.bool,
updateTitle: PropTypes.func.isRequired
};
class Profile extends Component {
componentWillMount() {
this.props.updateTitle('Profile');
}
componentDidUpdate() {
const { requestedUsername, currentUsername, paramsUsername } = this.props;
if (!requestedUsername && paramsUsername !== currentUsername) {
this.props.fetchOtherUser(paramsUsername);
}
}
renderRequestedProfile() {
const {
fetchOtherUserCompleted,
isLocked,
isUserFound,
isCurrentUserProfile,
paramsUsername,
showAbout,
showLocation,
showName,
showPoints,
showHeatMap,
showCerts,
showPortfolio,
showTimeLine
} = this.props;
const takeMeToChallenges = (
<a href='/challenges/current-challenge'>
<Button bsSize='lg' bsStyle='primary'>
Take me to the Challenges
</Button>
</a>
);
if (isLocked) {
return (
<div className='full-size'>
<h3>
{
`${paramsUsername} has not made their profile public. `
}
</h3>
<Alert bsStyle='info'>
<p>
{
'In order to view their freeCodeCamp certifications, ' +
'they need to make their profile public'
}
</p>
</Alert>
{ takeMeToChallenges }
</div>
);
}
if (!isCurrentUserProfile && (fetchOtherUserCompleted && !isUserFound)) {
return (
<div className='full-size'>
<Alert bsStyle='info'>
<p>
{ `We could not find a user by the name of "${paramsUsername}"` }
</p>
</Alert>
{ takeMeToChallenges }
</div>
);
}
return (
<div>
<CamperHOC
showAbout={ showAbout }
showLocation={ showLocation }
showName={ showName }
showPoints={ showPoints }
/>
{ showHeatMap ? <HeatMap /> : null }
{ showCerts ? <Certificates /> : null }
{ showPortfolio ? <Portfolio /> : null }
{ showTimeLine ? <Timeline className='timelime-container' /> : null }
</div>
);
}
renderReportUserButton() {
const {
isSignedIn,
fetchOtherUserCompleted,
isCurrentUserProfile,
isUserFound,
paramsUsername
} = this.props;
return (
!isSignedIn ||
isCurrentUserProfile ||
(fetchOtherUserCompleted && !isUserFound)
) ?
null :
(
<FullWidthRow>
<Button
block={ true }
bsSize='lg'
bsStyle='warning'
href={`/user/${paramsUsername}/report-user`}
>
Report Profile
</Button>
</FullWidthRow>
);
}
renderSettingsLink() {
const { isCurrentUserProfile } = this.props;
return isCurrentUserProfile ?
<FullWidthRow>
<Link to='/settings'>
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
className='btn-link-social'
>
Update my settings
</Button>
</Link>
</FullWidthRow> :
null;
}
render() {
const {
isCurrentUserProfile,
showLoading,
fetchOtherUserCompleted
} = this.props;
if (isCurrentUserProfile && showLoading) {
return <Loader />;
}
if (!isCurrentUserProfile && !fetchOtherUserCompleted) {
return <Loader />;
}
return (
<ChildContainer>
<Grid className={`${ns}-container`}>
{ this.renderSettingsLink() }
{ this.renderRequestedProfile() }
{ this.renderReportUserButton() }
</Grid>
</ChildContainer>
);
}
}
Profile.displayName = 'Profile';
Profile.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(Profile);

View File

@ -1,77 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import { userByNameSelector } from '../../../redux';
import Camper from '../../Settings/components/Camper.jsx';
const mapStateToProps = createSelector(
userByNameSelector,
({
name,
username,
location,
points,
picture,
about,
yearsTopContributor
}) => ({
name,
username,
location,
points,
picture,
about,
yearsTopContributor
})
);
const propTypes = {
about: PropTypes.string,
location: PropTypes.string,
name: PropTypes.string,
picture: PropTypes.string,
points: PropTypes.number,
showAbout: PropTypes.bool,
showLocation: PropTypes.bool,
showName: PropTypes.bool,
showPoints: PropTypes.bool,
username: PropTypes.string,
yearsTopContributor: PropTypes.array
};
function CamperHOC({
name,
username,
location,
points,
picture,
about,
yearsTopContributor,
showAbout,
showLocation,
showName,
showPoints
}) {
return (
<div>
<Camper
about={ showAbout ? about : '' }
location={ showLocation ? location : '' }
name={ showName ? name : '' }
picture={ picture }
points={ showPoints ? points : null }
username={ username }
yearsTopContributor={ yearsTopContributor }
/>
<hr />
</div>
);
}
CamperHOC.displayName = 'CamperHOC';
CamperHOC.propTypes = propTypes;
export default connect(mapStateToProps)(CamperHOC);

View File

@ -1,183 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import _ from 'lodash';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import {
Button,
Row,
Col
} from 'react-bootstrap';
import { FullWidthRow } from '../../../helperComponents';
import { userByNameSelector } from '../../../redux';
const mapStateToProps = createSelector(
userByNameSelector,
({
isRespWebDesignCert,
is2018DataVisCert,
isFrontEndLibsCert,
isJsAlgoDataStructCert,
isApisMicroservicesCert,
isInfosecQaCert,
isFrontEndCert,
isBackEndCert,
isDataVisCert,
isFullStackCert,
is2018FullStackCert,
username
}) => ({
username,
hasModernCert: (
isRespWebDesignCert ||
is2018DataVisCert ||
isFrontEndLibsCert ||
isJsAlgoDataStructCert ||
isApisMicroservicesCert ||
isInfosecQaCert
),
hasLegacyCert: (isFrontEndCert || isBackEndCert || isDataVisCert),
currentCerts: [
{
show: is2018FullStackCert,
title: 'Full Stack Certification',
showURL: '2018-full-stack'
},
{
show: isRespWebDesignCert,
title: 'Responsive Web Design Certification',
showURL: 'responsive-web-design'
},
{
show: isJsAlgoDataStructCert,
title: 'JavaScript Algorithms and Data Structures Certification',
showURL: 'javascript-algorithms-and-data-structures'
},
{
show: isFrontEndLibsCert,
title: 'Front End Libraries Certification',
showURL: 'front-end-libraries'
},
{
show: is2018DataVisCert,
title: 'Data Visualization Certification',
showURL: 'data-visualization'
},
{
show: isApisMicroservicesCert,
title: 'APIs and Microservices Certification',
showURL: 'apis-and-microservices'
},
{
show: isInfosecQaCert,
title: 'Information Security and Quality Assurance Certification',
showURL: 'information-security-and-quality-assurance'
}
],
legacyCerts: [
{
show: isFullStackCert,
title: 'Full Stack Certification',
showURL: 'legacy-full-stack'
},
{
show: isFrontEndCert,
title: 'Front End Certification',
showURL: 'legacy-front-end'
},
{
show: isBackEndCert,
title: 'Back End Certification',
showURL: 'legacy-back-end'
},
{
show: isDataVisCert,
title: 'Data Visualization Certification',
showURL: 'legacy-data-visualization'
}
]
})
);
function mapDispatchToProps() {
return {};
}
const certArrayTypes = PropTypes.arrayOf(
PropTypes.shape({
show: PropTypes.bool,
title: PropTypes.string,
showURL: PropTypes.string
})
);
const propTypes = {
currentCerts: certArrayTypes,
hasLegacyCert: PropTypes.bool,
hasModernCert: PropTypes.bool,
legacyCerts: certArrayTypes,
username: PropTypes.string
};
function renderCertShow(username, cert) {
return cert.show ? (
<Row key={ cert.showURL }>
<Col sm={ 10 } smPush={ 1 }>
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
href={ `/certification/${username}/${cert.showURL}`}
>
View {cert.title}
</Button>
</Col>
</Row>
) :
null;
}
function Certificates({
currentCerts,
legacyCerts,
hasLegacyCert,
hasModernCert,
username
}) {
const renderCertShowWithUsername = _.curry(renderCertShow)(username);
return (
<div>
<FullWidthRow>
<h2 className='text-center'>freeCodeCamp Certifications</h2>
<br />
{
hasModernCert ?
currentCerts.map(renderCertShowWithUsername) :
<p className='text-center' >
No certifications have been earned under the current curriculum
</p>
}
{
hasLegacyCert ?
<div>
<br />
<h3 className='text-center'>Legacy Certifications</h3>
<br />
{
legacyCerts.map(renderCertShowWithUsername)
}
</div> :
null
}
</FullWidthRow>
<hr />
</div>
);
}
Certificates.propTypes = propTypes;
Certificates.displayName = 'Certificates';
export default connect(mapStateToProps, mapDispatchToProps)(Certificates);

View File

@ -1,125 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import d3 from 'react-d3';
import CalHeatMap from 'cal-heatmap';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import differenceInCalendarMonths from 'date-fns/difference_in_calendar_months';
import { FullWidthRow } from '../../../helperComponents';
import { userByNameSelector } from '../../../redux';
function ensureD3() {
// CalHeatMap requires d3 to be available on window
if (typeof window !== 'undefined') {
if ('d3' in window) {
return;
} else {
window.d3 = d3;
}
return;
}
return;
}
const mapStateToProps = createSelector(
userByNameSelector,
({ calendar, streak }) => ({ calendar, streak })
);
const propTypes = {
calendar: PropTypes.object,
streak: PropTypes.shape({
current: PropTypes.number,
longest: PropTypes.number
})
};
class HeatMap extends Component {
constructor(props) {
super(props);
this.renderMap = this.renderMap.bind(this);
}
componentDidMount() {
ensureD3();
this.renderMap();
}
renderMap() {
const { calendar = {} } = this.props;
if (Object.keys(calendar).length === 0) {
return null;
}
const today = new Date();
const cal = new CalHeatMap();
const rectSelector = '#cal-heatmap > svg > svg.graph-legend > g > rect.r';
const calLegendTitles = ['0 items', '1 item', '2 items', '3 or more items'];
const firstTS = Object.keys(calendar)[0];
const sevenMonths = 1000 * 60 * 60 * 24 * 210;
// start should not be earlier than 7 months before now:
let start = (firstTS * 1000 + sevenMonths < Date.now())
? new Date(Date.now() - sevenMonths)
: new Date(firstTS * 1000);
const monthsSinceFirstActive = differenceInCalendarMonths(
today,
start
);
cal.init({
itemSelector: '#cal-heatmap',
domain: 'month',
subDomain: 'day',
domainDynamicDimension: true,
domainGutter: 5,
data: calendar,
cellSize: 15,
cellRadius: 3,
cellPadding: 2,
tooltip: true,
range: monthsSinceFirstActive < 12 ? monthsSinceFirstActive + 1 : 12,
start,
legendColors: ['#cccccc', '#006400'],
legend: [1, 2, 3],
label: {
position: 'top'
}
});
calLegendTitles.forEach(function(title, i) {
document
.querySelector(rectSelector + (i + 1).toString() + '> title')
.innerHTML = title;
});
return null;
}
render() {
const { streak = {} } = this.props;
return (
<div id='cal-heatmap-container'>
<Helmet>
<link href='/css/cal-heatmap.css' rel='stylesheet' />
</Helmet>
<FullWidthRow>
<div id='cal-heatmap' />
</FullWidthRow>
<FullWidthRow>
<div className='streak-container'>
<span className='streak'>
<strong>Longest Streak:</strong> { streak.longest || 1 }
</span>
<span className='streak'>
<strong>Current Streak:</strong> { streak.current || 1 }
</span>
</div>
</FullWidthRow>
<hr />
</div>
);
}
}
HeatMap.displayName = 'HeatMap';
HeatMap.propTypes = propTypes;
export default connect(mapStateToProps)(HeatMap);

View File

@ -1,74 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import { Thumbnail, Media } from 'react-bootstrap';
import { FullWidthRow } from '../../../helperComponents';
import { userByNameSelector } from '../../../redux';
const mapStateToProps = createSelector(
userByNameSelector,
({ portfolio }) => ({ portfolio })
);
const propTypes = {
portfolio: PropTypes.arrayOf(
PropTypes.shape({
description: PropTypes.string,
id: PropTypes.string,
image: PropTypes.string,
title: PropTypes.string,
url: PropTypes.string
})
)
};
function Portfolio({ portfolio = [] }) {
if (!portfolio.length) {
return null;
}
return (
<div>
<FullWidthRow>
<h2 className='text-center'>Portfolio</h2>
{
portfolio.map(({ title, url, image, description, id}) => (
<Media key={ id }>
<Media.Left align='middle'>
{
image && (
<a href={ url } rel='nofollow'>
<Thumbnail
alt={ `A screen shot of ${title}` }
src={ image }
style={{ width: '150px' }}
/>
</a>
)
}
</Media.Left>
<Media.Body>
<Media.Heading>
<a href={ url } rel='nofollow'>
{ title }
</a>
</Media.Heading>
<p>
{ description }
</p>
</Media.Body>
</Media>
))
}
</FullWidthRow>
<hr />
</div>
);
}
Portfolio.displayName = 'Portfolio';
Portfolio.propTypes = propTypes;
export default connect(mapStateToProps)(Portfolio);

View File

@ -1,143 +0,0 @@
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 FontAwesomeIcon from '@fortawesome/react-fontawesome';
import {
faLinkedin,
faGithub,
faTwitter
} from '@fortawesome/free-brands-svg-icons';
import { faLink } from '@fortawesome/free-solid-svg-icons';
import { userByNameSelector } from '../../../redux';
const propTypes = {
email: PropTypes.string,
githubProfile: PropTypes.string,
isGithub: PropTypes.bool,
isLinkedIn: PropTypes.bool,
isTwitter: PropTypes.bool,
isWebsite: PropTypes.bool,
linkedIn: PropTypes.string,
show: PropTypes.bool,
twitter: PropTypes.string,
website: PropTypes.string
};
const mapStateToProps = createSelector(
userByNameSelector,
({
githubProfile,
isLinkedIn,
isGithub,
isTwitter,
isWebsite,
linkedIn,
twitter,
website
}) => ({
githubProfile,
isLinkedIn,
isGithub,
isTwitter,
isWebsite,
linkedIn,
show: (isLinkedIn || isGithub || isTwitter || isWebsite),
twitter,
website
})
);
function mapDispatchToProps() {
return {};
}
function LinkedInIcon(linkedIn) {
return (
<a href={ linkedIn } rel='no-follow' target='_blank'>
<FontAwesomeIcon
icon={faLinkedin}
size='2x'
/>
</a>
);
}
function GithubIcon(ghURL) {
return (
<a href={ ghURL } rel='no-follow' target='_blank'>
<FontAwesomeIcon
icon={faGithub}
size='2x'
/>
</a>
);
}
function WebsiteIcon(website) {
return (
<a href={ website } rel='no-follow' target='_blank'>
<FontAwesomeIcon
icon={faLink}
size='2x'
/>
</a>
);
}
function TwitterIcon(handle) {
return (
<a href={ handle } rel='no-follow' target='_blank' >
<FontAwesomeIcon
icon={faTwitter}
size='2x'
/>
</a>
);
}
function SocialIcons(props) {
const {
githubProfile,
isLinkedIn,
isGithub,
isTwitter,
isWebsite,
linkedIn,
show,
twitter,
website
} = props;
if (!show) {
return null;
}
return (
<Row>
<Col
className='text-center social-media-icons'
sm={ 6 }
smOffset={ 3 }
>
{
isLinkedIn ? LinkedInIcon(linkedIn) : null
}
{
isGithub ? GithubIcon(githubProfile) : 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

@ -1,173 +0,0 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import format from 'date-fns/format';
import { find, reverse, sortBy } from 'lodash';
import {
Button,
Modal,
Table
} from 'react-bootstrap';
import { challengeIdToNameMapSelector } from '../../../entities';
import { userByNameSelector } from '../../../redux';
import { homeURL } from '../../../../utils/constantStrings.json';
import blockNameify from '../../../utils/blockNameify';
import { Link } from '../../../Router';
import { FullWidthRow } from '../../../helperComponents';
import SolutionViewer from '../../Settings/components/SolutionViewer.jsx';
const mapStateToProps = createSelector(
challengeIdToNameMapSelector,
userByNameSelector,
(
idToNameMap,
{ completedChallenges: completedMap = [], username }
) => ({
completedMap,
idToNameMap,
username
})
);
const propTypes = {
completedMap: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string,
completedDate: PropTypes.number,
challengeType: PropTypes.number,
solution: PropTypes.string,
files: PropTypes.shape({
ext: PropTypes.string,
contents: PropTypes.string
})
})
),
idToNameMap: PropTypes.objectOf(PropTypes.string),
username: PropTypes.string
};
class Timeline extends PureComponent {
constructor(props) {
super(props);
this.state = {
solutionToView: null,
solutionOpen: false
};
this.closeSolution = this.closeSolution.bind(this);
this.renderCompletion = this.renderCompletion.bind(this);
this.viewSolution = this.viewSolution.bind(this);
}
renderCompletion(completed) {
const { idToNameMap } = this.props;
const { id, completedDate } = completed;
const challengeDashedName = idToNameMap[id];
return (
<tr key={ id }>
<td>
<a href={`/challenges/${challengeDashedName}`}>
{ blockNameify(challengeDashedName) }
</a>
</td>
<td className='text-center'>
<time dateTime={ format(completedDate, 'YYYY-MM-DDTHH:MM:SSZ') }>
{
format(completedDate, 'MMMM D, YYYY')
}
</time>
</td>
<td/>
</tr>
);
}
viewSolution(id) {
this.setState(state => ({
...state,
solutionToView: id,
solutionOpen: true
}));
}
closeSolution() {
this.setState(state => ({
...state,
solutionToView: null,
solutionOpen: false
}));
}
render() {
const { completedMap, idToNameMap, username } = this.props;
const { solutionToView: id, solutionOpen } = this.state;
if (!Object.keys(idToNameMap).length) {
return null;
}
return (
<FullWidthRow>
<h2 className='text-center'>Timeline</h2>
{
completedMap.length === 0 ?
<p className='text-center'>
No challenges have been completed yet.&nbsp;
<Link to={ homeURL }>
Get started here.
</Link>
</p> :
<Table condensed={true} striped={true}>
<thead>
<tr>
<th>Challenge</th>
<th className='text-center'>Completed</th>
</tr>
</thead>
<tbody>
{
reverse(
sortBy(
completedMap,
[ 'completedDate' ]
).filter(({id}) => id in idToNameMap)
)
.map(this.renderCompletion)
}
</tbody>
</Table>
}
{
id &&
<Modal
aria-labelledby='contained-modal-title'
onHide={this.closeSolution}
show={ solutionOpen }
>
<Modal.Header closeButton={ true }>
<Modal.Title id='contained-modal-title'>
{ `${username}'s Solution to ${blockNameify(idToNameMap[id])}` }
</Modal.Title>
</Modal.Header>
<Modal.Body>
<SolutionViewer
solution={
find(completedMap, ({id: completedId}) => completedId === id)
}
/>
</Modal.Body>
<Modal.Footer>
<Button onClick={this.closeSolution}>Close</Button>
</Modal.Footer>
</Modal>
}
</FullWidthRow>
);
}
}
Timeline.displayName = 'Timeline';
Timeline.propTypes = propTypes;
export default connect(mapStateToProps)(Timeline);

View File

@ -1,7 +0,0 @@
import { types } from './redux';
export { default } from './Profile.jsx';
export const routes = {
[types.onRouteProfile]: '/:username'
};

View File

@ -1 +0,0 @@
"profile"

View File

@ -1,102 +0,0 @@
// should be the same as the filename and ./ns.json
@ns: profile;
.avatar-container {
margin: 0 auto;
text-align: center;
.avatar {
height: 180px;
}
}
.solution-viewer {
background-color: #fff;
}
.bigger-text {
font-size: 1.7rem;
}
.bio {
text-align: center;
}
.name, .location, .bio, .points, .yearsTopContributor {
font-size: 2rem;
margin-bottom: 0;
}
.@{ns}-container {
a:hover {
text-decoration: none;
text-decoration-line: none;
}
.full-size {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.row {
margin-bottom: 15px;
}
.social-media-icons {
a {
&:first-child {
margin-left: 0;
}
margin-left: 32px;
}
}
h2, h3 {
&.name, &.username, &.points, &.location {
margin-top: 0;
margin-bottom: 0;
}
}
.portfolio-description {
height: 62px;
}
.timeline-container {
display: flex;
align-items: center;
}
#cal-heatmap {
display: flex;
justify-content: center;
}
.streak-container {
display: flex;
justify-content: space-around;
align-items: center;
font-size: 18px;
}
.streak {
color: @brand-primary;
strong {
color: #333;
}
}
}
.night {
.@{ns}-container {
.streak {
strong {
color: #ccc;
}
}
}
}

View File

@ -1,31 +0,0 @@
import {
createAction,
createTypes
} from 'berkeleys-redux-utils';
import ns from '../ns.json';
import handleActions from 'berkeleys-redux-utils/lib/handle-actions';
export const types = createTypes([
'onRouteProfile',
'userFound'
], 'profile');
export const onRouteProfile = createAction(types.onRouteProfile);
export const userFound = createAction(types.userFound);
const initialState = {
isUserFound: null
};
export const userFoundSelector = state => state[ns].isUserFound;
export default handleActions(() => (
{
[types.userFound]: (state, { payload }) => ({
...state,
isUserFound: payload
})
}),
initialState,
ns
);

View File

@ -1,122 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Button } from 'react-bootstrap';
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 CertificationSettings from './components/Cert-Settings.jsx';
import PortfolioSettings from './components/Portfolio-Settings.jsx';
import PrivacySettings from './components/Privacy-Settings.jsx';
import Honesty from './components/Honesty.jsx';
import {
toggleNightMode,
updateTitle,
signInLoadingSelector,
usernameSelector,
themeSelector,
hardGoTo
} from '../../redux';
const mapStateToProps = createSelector(
usernameSelector,
themeSelector,
signInLoadingSelector,
(
username,
theme,
showLoading,
) => ({
showLoading,
username
})
);
const mapDispatchToProps = {
hardGoTo,
toggleNightMode,
updateTitle
};
const propTypes = {
hardGoTo: PropTypes.func.isRequired,
showLoading: PropTypes.bool,
updateTitle: PropTypes.func.isRequired,
username: PropTypes.string
};
export class Settings extends React.Component {
componentWillMount() {
this.props.updateTitle('Settings');
}
componentWillReceiveProps({ username, showLoading, hardGoTo }) {
if (!username && !showLoading) {
hardGoTo('/signup');
}
}
render() {
const {
showLoading,
username
} = this.props;
if (!username && showLoading) {
return <Loader />;
}
return (
<div>
<FullWidthRow>
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
className='btn-link-social'
href={ `/${username}`}
>
Show me my public portfolio
</Button>
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
className='btn-link-social'
href={ '/signout' }
>
Sign me out of freeCodeCamp
</Button>
</FullWidthRow>
<h1 className='text-center'>{ `Account Settings for ${username}` }</h1>
<AboutSettings />
<Spacer />
<PrivacySettings />
<Spacer />
<EmailSettings />
<Spacer />
<InternetSettings />
<Spacer />
<PortfolioSettings />
<Spacer />
<Honesty />
<Spacer />
<CertificationSettings />
<Spacer />
<DangerZone />
</div>
);
}
}
Settings.displayName = 'Settings';
Settings.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(Settings);

View File

@ -1,41 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Grid } from 'react-bootstrap';
import ns from './ns.json';
import { showUpdateEmailViewSelector } from './redux';
import Settings from './Settings.jsx';
import UpdateEmail from './routes/update-email';
import ChildContainer from '../../Child-Container.jsx';
const mapStateToProps = state => ({
showUpdateEmailView: showUpdateEmailViewSelector(state)
});
const propTypes = {
showUpdateEmailView: PropTypes.bool
};
export function ShowSettings({ showUpdateEmailView }) {
let ChildComponent = Settings;
if (showUpdateEmailView) {
ChildComponent = UpdateEmail;
}
return (
<ChildContainer>
<Grid className={ `${ns}-container` }>
<ChildComponent />
</Grid>
</ChildContainer>
);
}
ShowSettings.displayName = 'ShowSettings';
ShowSettings.propTypes = propTypes;
export default connect(
mapStateToProps
)(ShowSettings);

View File

@ -1,55 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ToggleButtonGroup as BSBG, ToggleButton as TB } from 'react-bootstrap';
import ns from './ns.json';
const propTypes = {
name: PropTypes.string.isRequired,
offLabel: PropTypes.string,
onChange: PropTypes.func.isRequired,
onLabel: PropTypes.string,
value: PropTypes.bool.isRequired
};
export default function ToggleButton({
name,
onChange,
value,
onLabel = 'On',
offLabel = 'Off'
}) {
return (
<div className={ `${ns}-container` }>
<BSBG
name={ name }
onChange={ onChange }
type='radio'
>
<TB
bsSize='lg'
bsStyle='primary'
className={ value && 'active' }
disabled={ value }
type='radio'
value={ 1 }
>
{ onLabel }
</TB>
<TB
bsSize='lg'
bsStyle='primary'
className={ !value && 'active' }
disabled={ !value }
type='radio'
value={ 2 }
>
{ offLabel }
</TB>
</BSBG>
</div>
);
}
ToggleButton.displayName = 'ToggleButton';
ToggleButton.propTypes = propTypes;

View File

@ -1 +0,0 @@
export { default } from './Toggle-Button.jsx';

View File

@ -1,21 +0,0 @@
@ns: toggle;
.@{ns}-container > .btn-group {
min-width: 180px;
float: right;
.btn {
margin-top: 20px;
}
.btn[disabled] {
opacity: 1;
}
@media (max-width: 768px) {
float: none;
.btn {
margin-top: 10px;
margin-bottom: 20px;
}
}
}

View File

@ -1,217 +0,0 @@
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 ThemeSettings from './ThemeSettings.jsx';
import Camper from './Camper.jsx';
import UsernameSettings from './UsernameSettings.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,
location,
name,
picture,
points,
theme,
username
},
) => ({
about,
currentTheme: theme,
initialValues: { name, location, about, picture },
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,
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,
toggleNightMode,
username
} = this.props;
const { view } = this.state;
const toggleTheme = () => toggleNightMode(username, currentTheme);
return (
<div className='about-settings'>
<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>
<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

@ -1,92 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'react-bootstrap';
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import { faAward } from '@fortawesome/free-solid-svg-icons';
import SocialIcons from '../../Profile/components/SocialIcons.jsx';
const propTypes = {
about: PropTypes.string,
location: PropTypes.string,
name: PropTypes.string,
picture: PropTypes.string,
points: PropTypes.number,
username: PropTypes.string,
yearsTopContributor: PropTypes.array
};
function pluralise(word, condition) {
return condition ? word + 's' : word;
}
function joinArray(array) {
return array.reduce((string, item, index, array) => {
if (string.length > 0) {
if (index === array.length - 1) {
return `${string} and ${item}`;
} else {
return `${string}, ${item}`;
}
} else {
return item;
}
});
}
function Camper({
name,
username,
location,
points,
picture,
about,
yearsTopContributor
}) {
return (
<div>
<Row>
<Col className='avatar-container' xs={ 12 }>
<img
alt={ username + '\'s profile picture' }
className='avatar'
src={ picture }
/>
</Col>
</Row>
<SocialIcons />
<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> }
{
typeof points === 'number' ? (
<p className='text-center points'>
{ `${points} ${pluralise('point', points !== 1)}` }
</p>
) : null
}
{ yearsTopContributor.filter(Boolean).length > 0 &&
(
<div>
<br/>
<p className='text-center yearsTopContributor'>
<FontAwesomeIcon
icon={faAward}
/> Top Contributor
</p>
<p className='text-center'>
{ joinArray(yearsTopContributor) }
</p>
</div>
)
}
<br/>
</div>
);
}
Camper.displayName = 'Camper';
Camper.propTypes = propTypes;
export default Camper;

View File

@ -1,336 +0,0 @@
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 {
userSelector,
hardGoTo,
createErrorObservable
} from '../../../redux';
import {
buildUserProjectsMap,
jsProjectSuperBlock
} from '../utils/buildUserProjectsMap';
const mapStateToProps = createSelector(
userSelector,
projectsSelector,
(
{
completedChallenges,
isRespWebDesignCert,
is2018DataVisCert,
isFrontEndLibsCert,
isJsAlgoDataStructCert,
isApisMicroservicesCert,
isInfosecQaCert,
isFrontEndCert,
isBackEndCert,
isDataVisCert,
isFullStackCert,
username
},
projects
) => {
let modernProjects = projects.filter(p => !p.superBlock.includes('legacy'));
modernProjects.push(modernProjects.shift());
return {
allProjects: projects,
legacyProjects: projects.filter(p => p.superBlock.includes('legacy')),
modernProjects: modernProjects,
userProjects: projects
.map(block => buildUserProjectsMap(block, completedChallenges))
.reduce((projects, current) => ({
...projects,
...current
}), {}),
blockNameIsCertMap: {
'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,
'APIs and Microservices Projects': isApisMicroservicesCert,
'Information Security and Quality Assurance Projects': isInfosecQaCert,
'Full Stack Certification': isFullStackCert,
'Legacy Front End Projects': isFrontEndCert,
'Legacy Back End Projects': isBackEndCert,
'Legacy Data Visualization Projects': isDataVisCert
},
username
};
}
);
function mapDispatchToProps(dispatch) {
return bindActionCreators({
claimCert,
createError: createErrorObservable,
hardGoTo,
updateUserBackend
}, dispatch);
}
const projectsTypes = PropTypes.arrayOf(
PropTypes.shape({
projectBlockName: PropTypes.string,
challenges: PropTypes.arrayOf(
PropTypes.shape({
dashedName: PropTypes.string,
id: PropTypes.string,
title: PropTypes.string
})
)
}),
);
const propTypes = {
allProjects: projectsTypes,
blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool),
claimCert: PropTypes.func.isRequired,
createError: PropTypes.func.isRequired,
hardGoTo: PropTypes.func.isRequired,
legacyProjects: projectsTypes,
modernProjects: projectsTypes,
superBlock: PropTypes.string,
updateUserBackend: PropTypes.func.isRequired,
userProjects: PropTypes.objectOf(
PropTypes.oneOfType(
[
// this is really messy, it should be addressed
// in completedChallenges migration to unify to one type
PropTypes.string,
PropTypes.arrayOf(PropTypes.object),
PropTypes.object
]
)
),
username: PropTypes.string
};
class CertificationSettings extends PureComponent {
constructor(props) {
super(props);
this.buildProjectForms = this.buildProjectForms.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
buildProjectForms({
projectBlockName,
challenges,
superBlock
}) {
const {
blockNameIsCertMap,
claimCert,
hardGoTo,
userProjects,
username
} = this.props;
const isCertClaimed = blockNameIsCertMap[projectBlockName];
const challengeTitles = challenges
.map(challenge => challenge.title || 'Unknown Challenge');
if (superBlock === jsProjectSuperBlock) {
return (
<JSAlgoAndDSForm
challenges={ challengeTitles }
claimCert={ claimCert }
hardGoTo={ hardGoTo }
isCertClaimed={ isCertClaimed }
jsProjects={ userProjects[superBlock] }
key={ superBlock }
projectBlockName={ projectBlockName }
superBlock={ superBlock }
username={ username }
/>
);
}
const options = challengeTitles
.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 = challengeTitles
.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 === challengeTitles.length;
let isFullStack = superBlock === 'full-stack';
let isFullStackClaimable = false;
let description = '';
if (isFullStack) {
isFullStackClaimable = Object.keys(blockNameIsCertMap).every(function(e) {
if (e.indexOf('Full Stack') !== -1 || e.indexOf('Legacy') !== -1) {
return true;
}
return blockNameIsCertMap[e];
});
description = (
<div>
<p>
Once you've earned the following freeCodeCamp certifications,
you'll be able to claim The Full Stack Developer Certification:
</p>
<ul>
<li>Responsive Web Design</li>
<li>Algorithms and Data Structures</li>
<li>Front End Libraries</li>
<li>Data Visualization</li>
<li>APIs and Microservices</li>
<li>Information Security and Quality Assurance</li>
</ul>
</div>
);
}
return (
<FullWidthRow key={superBlock}>
<h3 className='project-heading'>{ projectBlockName }</h3>
{description}
<Form
buttonText={ fullForm || isFullStack
? 'Claim Certification' : 'Save Progress' }
enableSubmit={ isFullStack ? isFullStackClaimable : fullForm }
formFields={ challengeTitles.concat([ 'id' ]) }
hideButton={isCertClaimed}
id={ superBlock }
initialValues={{
...initialValues,
...userValues
}}
options={ options }
submit={ this.handleSubmit }
/>
{
isCertClaimed ?
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
href={ `/certification/${username}/${superBlock}`}
target='_blank'
>
Show Certification
</Button> :
null
}
<hr />
</FullWidthRow>
);
}
handleSubmit(values) {
const { id } = values;
const { allProjects } = this.props;
let project = _.find(allProjects, { superBlock: id });
if (!project) {
// the submitted projects do not belong to current/legacy certifications
return this.props.createError(
new Error(
'Submitted projects do not belong to either current or ' +
'legacy certifications'
)
);
}
const valuesSaved = _.values(this.props.userProjects[id])
.filter(Boolean)
.filter(_.isString);
// minus 1 due to the form id being in values
const isProjectSectionComplete =
(valuesSaved.length - 1) === project.challenges.length;
if (isProjectSectionComplete) {
return this.props.claimCert(id);
}
const valuesToIds = project.challenges
.reduce((valuesMap, current) => {
const solution = values[current.title];
if (solution) {
return {
...valuesMap,
[current.id]: solution
};
}
return valuesMap;
}, {});
return this.props.updateUserBackend({
projects: {
[id]: valuesToIds
}
});
}
render() {
const {
modernProjects,
legacyProjects
} = this.props;
if (!modernProjects.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 certification,
you can claim it.
</p>
</FullWidthRow>
{
modernProjects.map(this.buildProjectForms)
}
<SectionHeader>
Legacy Certification Settings
</SectionHeader>
{
legacyProjects.map(this.buildProjectForms)
}
</div>
);
}
}
CertificationSettings.displayName = 'CertificationSettings';
CertificationSettings.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(CertificationSettings);

View File

@ -1,105 +0,0 @@
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>
Please be careful! Changes in this section are permanent.
</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

@ -1,71 +0,0 @@
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

@ -1,170 +0,0 @@
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,
updateUserBackend
} = 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'>
Your email has not been verified.
To use your email, you must
<a href='/update-email'> verify it here first</a>.
</Alert>
</HelpBlock>
</FullWidthRow>
}
<FullWidthRow>
<EmailForm
email={ email }
initialValues={{ email, confirmEmail: '' }}
/>
</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

@ -1,170 +0,0 @@
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 }>
Current Email
</Col>
<Col sm={ 9 } xs={ 12 }>
<h4>
{ this.props.email }
</h4>
</Col>
</Row>
<Row className='inline-form-field'>
<Col sm={ 3 } xs={ 12 }>
<ControlLabel htmlFor='email'>
New 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 New 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

@ -1,95 +0,0 @@
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 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

@ -1,113 +0,0 @@
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,
({
githubProfile = '',
linkedin = '',
twitter = '',
website = ''
}) => ({
initialValues: {
githubProfile,
linkedin,
twitter,
personalWebsite: website
}
})
);
const formFields = [
'githubProfile',
'linkedin',
'twitter',
'personalWebsite'
];
function mapDispatchToProps(dispatch) {
return bindActionCreators({
updateUserBackend
}, dispatch);
}
const propTypes = {
fields: PropTypes.object,
githubProfile: PropTypes.string,
handleSubmit: PropTypes.func.isRequired,
linkedin: PropTypes.string,
personalWebsite: PropTypes.string,
twitter: PropTypes.string,
updateUserBackend: PropTypes.func.isRequired,
username: PropTypes.string
};
class InternetSettings extends PureComponent {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit({personalWebsite, ...others}) {
this.props.updateUserBackend({ ...others, website: personalWebsite });
}
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

@ -1,134 +0,0 @@
import React, { PureComponent } from 'react';
import { kebabCase, defaultTo } 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.oneOfType([
PropTypes.arrayOf(PropTypes.object),
PropTypes.string
])
),
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(`/certification/${username}/${superBlock}`);
}
return this.props.claimCert(superBlock);
}
render() {
const {
projectBlockName,
challenges = [],
jsProjects = {},
isCertClaimed
} = this.props;
const completeCount = Object.values(jsProjects)
.map(val => defaultTo(val, {}))
.filter(challengeInfo => Object.keys(challengeInfo).length !== 0)
.length;
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
rel='noopener noreferrer'
target='_blank'
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 === completeCount ?
<form onSubmit={ this.handleSubmit }>
<BlockSaveButton>
{ isCertClaimed ? 'Show' : 'Claim'} Certification
</BlockSaveButton>
</form> :
null
}
<hr />
</FullWidthRow>
);
}
}
JSAlgoAndDSForm.displayName = 'JSAlgoAndDSForm';
JSAlgoAndDSForm.propTypes = jsFormPropTypes;
export default JSAlgoAndDSForm;

View File

@ -1,178 +0,0 @@
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

@ -1,177 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Button } from 'react-bootstrap';
import { updateMyProfileUI } from '../redux';
import { userSelector } from '../../../redux';
import { FullWidthRow, Spacer } from '../../../helperComponents';
import SectionHeader from './SectionHeader.jsx';
import ToggleSetting from './ToggleSetting.jsx';
const mapStateToProps = createSelector(
userSelector,
user => ({
...user.profileUI,
user
})
);
const mapDispatchToProps = dispatch =>
bindActionCreators({ updateMyProfileUI }, dispatch);
const propTypes = {
isLocked: PropTypes.bool,
showAbout: PropTypes.bool,
showCerts: PropTypes.bool,
showDonation: PropTypes.bool,
showHeatMap: PropTypes.bool,
showLocation: PropTypes.bool,
showName: PropTypes.bool,
showPoints: PropTypes.bool,
showPortfolio: PropTypes.bool,
showTimeLine: PropTypes.bool,
updateMyProfileUI: PropTypes.func.isRequired,
user: PropTypes.object
};
function PrivacySettings(props) {
const {
isLocked = true,
showAbout = false,
showCerts = false,
showDonation = false,
showHeatMap = false,
showLocation = false,
showName = false,
showPoints = false,
showPortfolio = false,
showTimeLine = false,
updateMyProfileUI,
user
} = props;
const toggleFlag = flag =>
() => updateMyProfileUI({ profileUI: { [flag]: !props[flag] } });
return (
<div className='privacy-settings'>
<SectionHeader>Privacy Settings</SectionHeader>
<FullWidthRow>
<p>
The settings in this section enable you to control what is shown on{' '}
your freeCodeCamp public portfolio.
</p>
<p>There is also a button to see what data we hold on your account</p>
<ToggleSetting
action='My profile'
explain={
'While your profile is completely private, no one will be able to ' +
'see your certifications'
}
flag={ isLocked }
flagName='isLocked'
offLabel='Public'
onLabel='Private'
toggleFlag={ toggleFlag('isLocked') }
/>
<ToggleSetting
action='My name'
flag={ !showName }
flagName='name'
offLabel='Public'
onLabel='Private'
toggleFlag={ toggleFlag('showName') }
/>
<ToggleSetting
action='My location'
flag={ !showLocation }
flagName='showLocation'
offLabel='Public'
onLabel='Private'
toggleFlag={ toggleFlag('showLocation') }
/>
<ToggleSetting
action='My "about me"'
flag={ !showAbout }
flagName='showAbout'
offLabel='Public'
onLabel='Private'
toggleFlag={ toggleFlag('showAbout') }
/>
<ToggleSetting
action='My points'
flag={ !showPoints }
flagName='showPoints'
offLabel='Public'
onLabel='Private'
toggleFlag={ toggleFlag('showPoints') }
/>
<ToggleSetting
action='My heat map'
flag={ !showHeatMap }
flagName='showHeatMap'
offLabel='Public'
onLabel='Private'
toggleFlag={ toggleFlag('showHeatMap') }
/>
<ToggleSetting
action='My certifications'
explain='Your certifications will be disabled'
flag={ !showCerts }
flagName='showCerts'
offLabel='Public'
onLabel='Private'
toggleFlag={ toggleFlag('showCerts') }
/>
<ToggleSetting
action='My portfolio'
flag={ !showPortfolio }
flagName='showPortfolio'
offLabel='Public'
onLabel='Private'
toggleFlag={ toggleFlag('showPortfolio') }
/>
<ToggleSetting
action='My time line'
explain='Your certifications will be disabled'
flag={ !showTimeLine }
flagName='showTimeLine'
offLabel='Public'
onLabel='Private'
toggleFlag={ toggleFlag('showTimeLine') }
/>
<ToggleSetting
action='My donations'
flag={ !showDonation }
flagName='showPortfolio'
offLabel='Public'
onLabel='Private'
toggleFlag={ toggleFlag('showDonation') }
/>
</FullWidthRow>
<FullWidthRow>
<Spacer />
<Button
block={true}
bsSize='lg'
bsStyle='primary'
download={`${user.username}.json`}
href={
`data:text/json;charset=utf-8,${
encodeURIComponent(JSON.stringify(user))
}`
}
>
Download all your data
</Button>
</FullWidthRow>
</div>
);
}
PrivacySettings.displayName = 'PrivacySettings';
PrivacySettings.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(PrivacySettings);

View File

@ -1,65 +0,0 @@
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 certifications 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

@ -1,25 +0,0 @@
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 className='text-center'>{ children }</h2>
<hr />
</FullWidthRow>
);
}
SectionHeader.displayName = 'SectionHeader';
SectionHeader.propTypes = propTypes;
export default SectionHeader;

View File

@ -1,80 +0,0 @@
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 getContentString(file) {
return file.trim() || '// We do not have the solution to this challenge';
}
function SolutionViewer({ files }) {
const solutions = files && Array.isArray(files) ?
files.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>
)) : (
<Panel
bsStyle='primary'
className='solution-viewer'
header='JS'
key={ files.slice(0, 10) }
>
<pre>
<code
className='language-markup'
dangerouslySetInnerHTML={{
__html: Prism.highlight(
getContentString(files),
Prism.languages.js
)
}}
/>
</pre>
</Panel>
)
;
return (
<div>
<Helmet>
<link href='/css/prism.css' rel='stylesheet' />
</Helmet>
{
solutions
}
</div>
);
}
SolutionViewer.displayName = 'SolutionViewer';
SolutionViewer.propTypes = {
files: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.objectOf(PropTypes.string)),
PropTypes.string
])
};
export default SolutionViewer;

View File

@ -1,38 +0,0 @@
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

@ -1,55 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import {
Row,
Col,
ControlLabel
} from 'react-bootstrap';
import TB from '../Toggle-Button';
const propTypes = {
action: PropTypes.string.isRequired,
explain: PropTypes.string,
flag: PropTypes.bool.isRequired,
flagName: PropTypes.string.isRequired,
toggleFlag: PropTypes.func.isRequired
};
export default function ToggleSetting({
action,
explain,
flag,
flagName,
toggleFlag,
...restProps
}) {
return (
<Row className='inline-form'>
<Col sm={ 8 } xs={ 12 }>
<ControlLabel className='toggle-label' htmlFor={ flagName }>
<p>
<strong>
{ action }
</strong>
<br />
{
explain ? <em>{explain}</em> : null
}
</p>
</ControlLabel>
</Col>
<Col sm={ 4 } xs={ 12 }>
<TB
name={ flagName }
onChange={ toggleFlag }
value={ flag }
{...restProps}
/>
</Col>
</Row>
);
}
ToggleSetting.displayName = 'ToggleSetting';
ToggleSetting.propTypes = propTypes;

View File

@ -1,187 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,23 +0,0 @@
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

@ -1,87 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { reduxForm } from 'redux-form';
import { FormFields, BlockSaveButton } 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 }
formId={ id }
options={ options }
/>
{
hideButton ?
null :
<BlockSaveButton
disabled={
allPristine && !enableSubmit ||
(!!Object.keys(errors).filter(key => errors[key]).length)
}
>
{
buttonText ? buttonText : null
}
</BlockSaveButton>
}
</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

@ -1,100 +0,0 @@
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,
formId: PropTypes.string,
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, formId, 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 = formId ?
`${formId}_${_.kebabCase(name)}` :
_.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

@ -1,35 +0,0 @@
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';
}

Some files were not shown because too many files have changed in this diff Show More