chore(root): Ensure development environment
This commit is contained in:
committed by
mrugesh mohapatra
parent
46a217d0a5
commit
dc00eb8555
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,7 @@
|
||||
node_modules
|
||||
|
||||
.env
|
||||
|
||||
*lib-cov
|
||||
*~
|
||||
*.seed
|
||||
|
@ -310,13 +310,13 @@ mongod
|
||||
npm run only-once
|
||||
|
||||
# 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)
|
||||
# Use this command instead
|
||||
# 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
|
||||
npm run develop-server
|
||||
node develop-client-server.js
|
||||
|
||||
```
|
||||
|
||||
|
@ -1,12 +1,17 @@
|
||||
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: [
|
||||
[
|
||||
require.resolve('@babel/preset-env'), {
|
||||
require.resolve('@babel/preset-env'),
|
||||
{
|
||||
targets: {
|
||||
node: 'current'
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
node: '8',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
|
1
api-server/.gitignore
vendored
1
api-server/.gitignore
vendored
@ -22,6 +22,7 @@ tmp
|
||||
|
||||
npm-debug.log
|
||||
node_modules
|
||||
compiled
|
||||
.idea
|
||||
*.iml
|
||||
.DS_Store
|
||||
|
@ -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);
|
@ -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;
|
@ -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);
|
@ -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;
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
export { default } from './Flash.jsx';
|
@ -1 +0,0 @@
|
||||
"flash"
|
@ -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)))
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
||||
);
|
@ -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
|
||||
);
|
@ -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);
|
||||
});
|
||||
}
|
@ -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
|
||||
);
|
||||
|
@ -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 };
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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;
|
@ -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';
|
@ -1 +0,0 @@
|
||||
export default from './Nav.jsx';
|
@ -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"
|
||||
}
|
||||
]
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default {
|
||||
clickOnLogo: PropTypes.func.isRequired
|
||||
};
|
@ -1 +0,0 @@
|
||||
"nav"
|
@ -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
|
||||
);
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export default from './Not-Found.jsx';
|
@ -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);
|
@ -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);
|
||||
}
|
||||
};
|
@ -1 +0,0 @@
|
||||
export { default as Link } from './Link.jsx';
|
@ -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;
|
@ -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 '#';
|
||||
};
|
@ -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);
|
@ -1 +0,0 @@
|
||||
export default from './Toasts.jsx';
|
@ -1 +0,0 @@
|
||||
"toasts"
|
@ -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
|
||||
);
|
@ -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
|
||||
}
|
||||
});
|
@ -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;
|
||||
}
|
@ -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
|
||||
});
|
||||
}
|
@ -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
|
||||
)
|
||||
);
|
@ -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
|
||||
];
|
@ -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';
|
@ -1,4 +0,0 @@
|
||||
&{ @import "./app.less"; }
|
||||
&{ @import "./Nav/nav.less"; }
|
||||
&{ @import "./Flash/flash.less"; }
|
||||
&{ @import "./routes/index.less"; }
|
@ -1 +0,0 @@
|
||||
"app"
|
@ -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>
|
||||
);
|
||||
}
|
@ -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
|
||||
);
|
@ -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
|
||||
);
|
@ -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
|
||||
);
|
@ -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);
|
||||
});
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import flowRight from 'lodash/flowRight';
|
||||
import { createNameIdMap } from '../../utils/map.js';
|
||||
|
||||
export const shapeChallenges = flowRight(
|
||||
entities => ({
|
||||
...entities,
|
||||
...createNameIdMap(entities)
|
||||
})
|
||||
);
|
@ -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]: '/'
|
||||
};
|
@ -1 +0,0 @@
|
||||
in case we ever want an admin panel
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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.
|
||||
<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);
|
@ -1,7 +0,0 @@
|
||||
import { types } from './redux';
|
||||
|
||||
export { default } from './Profile.jsx';
|
||||
|
||||
export const routes = {
|
||||
[types.onRouteProfile]: '/:username'
|
||||
};
|
@ -1 +0,0 @@
|
||||
"profile"
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
@ -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);
|
@ -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);
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export { default } from './Toggle-Button.jsx';
|
@ -1 +0,0 @@
|
||||
"toggle"
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
@ -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;
|
@ -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);
|
@ -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);
|
@ -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:  
|
||||
<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;
|
@ -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'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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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;
|
@ -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);
|
@ -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);
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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
Reference in New Issue
Block a user