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
|
node_modules
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
*lib-cov
|
*lib-cov
|
||||||
*~
|
*~
|
||||||
*.seed
|
*.seed
|
||||||
|
@ -310,13 +310,13 @@ mongod
|
|||||||
npm run only-once
|
npm run only-once
|
||||||
|
|
||||||
# Start the application without a backend server
|
# Start the application without a backend server
|
||||||
npm run develop
|
cd ./client && npm run develop
|
||||||
|
|
||||||
# If you require the backend server to be operational (persisted user interations/api calls)
|
# If you require the backend server to be operational (persisted user interations/api calls)
|
||||||
# Use this command instead
|
# Use this command instead
|
||||||
# Note: This command requires that you have a correctly seeded mongodb instance running
|
# Note: This command requires that you have a correctly seeded mongodb instance running
|
||||||
# Note: If you are runnoing the backend server inside a docker container, use the command above
|
# Note: If you are runnoing the backend server inside a docker container, use the command above
|
||||||
npm run develop-server
|
node develop-client-server.js
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [require.resolve('babel-plugin-transform-function-bind')],
|
plugins: [
|
||||||
|
require.resolve('babel-plugin-transform-function-bind'),
|
||||||
|
require.resolve('@babel/plugin-proposal-class-properties'),
|
||||||
|
require.resolve('@babel/plugin-proposal-object-rest-spread')
|
||||||
|
],
|
||||||
presets: [
|
presets: [
|
||||||
[
|
[
|
||||||
require.resolve('@babel/preset-env'), {
|
require.resolve('@babel/preset-env'),
|
||||||
|
{
|
||||||
targets: {
|
targets: {
|
||||||
node: 'current'
|
node: '8',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
1
api-server/.gitignore
vendored
1
api-server/.gitignore
vendored
@ -22,6 +22,7 @@ tmp
|
|||||||
|
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
node_modules
|
node_modules
|
||||||
|
compiled
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
.DS_Store
|
.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