feat: prep for modern challenges (#15781)
* feat(seed): Add modern challenge * chore(react): Use prop-types package * feat: Initial refactor to redux-first-router BREAKING CHANGE: Everything is different! * feat: First rendering * feat(routes): Challenges view render but failing * fix(Challenges): Remove contain HOC * fix(RFR): Add params selector * fix(RFR): :en should be :lang * fix: Update berks utils for redux * fix(Map): Challenge link to arg * fix(Map): Add trailing slash to map page * fix(RFR): Use FCC Link Use fcc Link to get around issue of lang being undefined * fix(Router): Link to is required * fix(app): Rely on RFR state for app lang * chore(RFR): Remove unused RFR Link * fix(RFR): Hydrate initial challenge using RFR and RO * fix: Casing issue * fix(RFR): Undefined links * fix(RFR): Use onRoute<name> convention for route types * feat(server/react): Add helpful redux logging/throwing * fix(server/react): Strip out nonjson from state This prevents thunks in routesMap from breaking serialization * fix(RFR/Link): Should accept any renderable * fix(RFR): Get redirects working * fix(RFR): Redirects and not found's * fix(Map): Move challenge onClick handler * fix(Map): Allow Router.link to handle clicks after onClick * fix(routes): Remove react-router-redux * feat(Router): Add lang to all route actions by default * fix(entities): Only fetch challenge if not already loaded * fix(Files): Move files to own feature * chore(Challenges): Remove vestigial hints logic * fix(RFR): Update challenges on route challenges * fix(code-storage): Should use events instead of commands * fix(Map): ClickOnMap should not hold on to event * chore(lint): Use eslint-config-freecodecamp Closes #15938 * feat(Panes): Update panes on route instead of render * fix(Panes): Store panesmap and update on fetchchallenges * fix(Panes): Normalize panesmaps * fix(Panes): Remove filter from createpanemap * fix(Panes): Middleware on location meta object * feat(Panes): Filter preview on nonhtml challenges * build(babel): Add lodash babel plugin * chore(lint): Lint js files * fix(server/user-stats): Remove use of lodash chain this interferes with babel-plugin-lodash * feat(dev): Add remote redux devtools for ssr * fix(Panes): Dispatch mount action this is needed to trigger window/divider epics * fix(Panes): Getpane to use new panesmap format * fix(Panes): Always update panes after state this lets the panes logic be affected by changes in state
This commit is contained in:
committed by
Quincy Larson
parent
2e46e60557
commit
dbecdc5618
5
.babelrc
5
.babelrc
@ -1,4 +1,7 @@
|
||||
{
|
||||
"presets": ["es2015", "react", "stage-0"],
|
||||
"plugins": ["babel-plugin-add-module-exports"]
|
||||
"plugins": [
|
||||
"babel-plugin-add-module-exports",
|
||||
"lodash"
|
||||
]
|
||||
}
|
||||
|
206
.eslintrc
206
.eslintrc
@ -1,207 +1,3 @@
|
||||
{
|
||||
"parserOption": {
|
||||
"ecmaVersion": 6,
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"mocha": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react",
|
||||
"import",
|
||||
"prefer-object-spread"
|
||||
],
|
||||
"settings": {
|
||||
"import/ignore": [
|
||||
"node_modules",
|
||||
"\\.json$"
|
||||
],
|
||||
"import/extensions": [
|
||||
".js",
|
||||
".jsx"
|
||||
]
|
||||
},
|
||||
"globals": {
|
||||
"Promise": true,
|
||||
"window": true,
|
||||
"$": true,
|
||||
"ga": true,
|
||||
"jQuery": true,
|
||||
"router": true
|
||||
},
|
||||
"rules": {
|
||||
"block-scoped-var": 0,
|
||||
"brace-style": [ 2, "1tbs", { "allowSingleLine": true } ],
|
||||
"camelcase": 2,
|
||||
"comma-dangle": 2,
|
||||
"comma-spacing": [ 2, { "before": false, "after": true } ],
|
||||
"comma-style": [ 2, "last" ],
|
||||
"complexity": 0,
|
||||
"consistent-return": 2,
|
||||
"consistent-this": 0,
|
||||
"curly": 2,
|
||||
"default-case": 2,
|
||||
"dot-notation": 0,
|
||||
"eol-last": 2,
|
||||
"eqeqeq": 2,
|
||||
"func-names": 0,
|
||||
"func-style": 0,
|
||||
"guard-for-in": 2,
|
||||
"handle-callback-err": 2,
|
||||
"jsx-quotes": [ 2, "prefer-single" ],
|
||||
"key-spacing": [ 2, { "beforeColon": false, "afterColon": true } ],
|
||||
"keyword-spacing": [ 2 ],
|
||||
"max-depth": 0,
|
||||
"max-len": [ 2, 80, 2 ],
|
||||
"max-nested-callbacks": 0,
|
||||
"max-params": 0,
|
||||
"max-statements": 0,
|
||||
"new-cap": 0,
|
||||
"new-parens": 2,
|
||||
"no-alert": 2,
|
||||
"no-array-constructor": 2,
|
||||
"no-bitwise": 2,
|
||||
"no-caller": 2,
|
||||
"no-catch-shadow": 2,
|
||||
"no-cond-assign": 2,
|
||||
"no-console": 0,
|
||||
"no-constant-condition": 2,
|
||||
"no-control-regex": 2,
|
||||
"no-debugger": 2,
|
||||
"no-delete-var": 2,
|
||||
"no-div-regex": 2,
|
||||
"no-dupe-keys": 2,
|
||||
"no-else-return": 0,
|
||||
"no-empty": 2,
|
||||
"no-empty-character-class": 2,
|
||||
"no-eq-null": 2,
|
||||
"no-eval": 2,
|
||||
"no-ex-assign": 2,
|
||||
"no-extend-native": 2,
|
||||
"no-extra-bind": 2,
|
||||
"no-extra-boolean-cast": 2,
|
||||
"no-extra-parens": 0,
|
||||
"no-extra-semi": 2,
|
||||
"no-fallthrough": 2,
|
||||
"no-floating-decimal": 2,
|
||||
"no-func-assign": 2,
|
||||
"no-implied-eval": 2,
|
||||
"no-inline-comments": 2,
|
||||
"no-inner-declarations": 2,
|
||||
"no-invalid-regexp": 2,
|
||||
"no-irregular-whitespace": 2,
|
||||
"no-iterator": 2,
|
||||
"no-label-var": 2,
|
||||
"no-labels": 2,
|
||||
"no-lone-blocks": 2,
|
||||
"no-lonely-if": 2,
|
||||
"no-loop-func": 2,
|
||||
"no-mixed-requires": 0,
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-multi-spaces": 2,
|
||||
"no-multi-str": 2,
|
||||
"no-multiple-empty-lines": [ 2, { "max": 2 } ],
|
||||
"no-native-reassign": 2,
|
||||
"no-negated-in-lhs": 2,
|
||||
"no-nested-ternary": 2,
|
||||
"no-new": 2,
|
||||
"no-new-func": 2,
|
||||
"no-new-object": 2,
|
||||
"no-new-require": 2,
|
||||
"no-new-wrappers": 2,
|
||||
"no-obj-calls": 2,
|
||||
"no-octal": 2,
|
||||
"no-octal-escape": 2,
|
||||
"no-path-concat": 2,
|
||||
"no-plusplus": 0,
|
||||
"no-process-env": 0,
|
||||
"no-process-exit": 2,
|
||||
"no-proto": 2,
|
||||
"no-redeclare": 2,
|
||||
"no-regex-spaces": 2,
|
||||
"no-reserved-keys": 0,
|
||||
"no-restricted-modules": 0,
|
||||
"no-return-assign": 2,
|
||||
"no-script-url": 2,
|
||||
"no-self-compare": 2,
|
||||
"no-sequences": 2,
|
||||
"no-shadow": 0,
|
||||
"no-shadow-restricted-names": 2,
|
||||
"no-spaced-func": 2,
|
||||
"no-sparse-arrays": 2,
|
||||
"no-sync": 0,
|
||||
"no-ternary": 0,
|
||||
"no-trailing-spaces": 2,
|
||||
"no-undef": 2,
|
||||
"no-undef-init": 2,
|
||||
"no-undefined": 2,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-unreachable": 2,
|
||||
"no-unused-expressions": 2,
|
||||
"no-unused-vars": 2,
|
||||
"no-use-before-define": 0,
|
||||
"no-void": 0,
|
||||
"no-warning-comments": [ 2, { "terms": [ "fixme" ], "location": "start" } ],
|
||||
"no-with": 2,
|
||||
"one-var": 0,
|
||||
"operator-assignment": 0,
|
||||
"padded-blocks": 0,
|
||||
"quote-props": [ 2, "as-needed" ],
|
||||
"quotes": [ 2, "single", "avoid-escape" ],
|
||||
"radix": 2,
|
||||
"semi": [ 2, "always" ],
|
||||
"semi-spacing": [ 2, { "before": false, "after": true } ],
|
||||
"sort-vars": 0,
|
||||
"space-before-blocks": [ 2, "always" ],
|
||||
"space-before-function-paren": [ 2, "never" ],
|
||||
"space-in-brackets": 0,
|
||||
"space-in-parens": 0,
|
||||
"space-infix-ops": 2,
|
||||
"space-unary-ops": [ 2, { "words": true, "nonwords": false } ],
|
||||
"spaced-comment": [ 2, "always", { "exceptions": [ "-" ] } ],
|
||||
"strict": 0,
|
||||
"use-isnan": 2,
|
||||
"valid-jsdoc": 2,
|
||||
"valid-typeof": 2,
|
||||
"vars-on-top": 0,
|
||||
"wrap-iife": [ 2, "any" ],
|
||||
"wrap-regex": 2,
|
||||
"yoda": 0,
|
||||
|
||||
"react/display-name": 2,
|
||||
"react/jsx-boolean-value": [ 2, "always" ],
|
||||
"react/jsx-closing-bracket-location": [ 2, { "selfClosing": "line-aligned", "nonEmpty": "props-aligned" } ],
|
||||
"react/jsx-no-undef": 2,
|
||||
"react/jsx-sort-props": [ 2, { "ignoreCase": true } ],
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
"react/jsx-wrap-multilines": 2,
|
||||
"react/no-did-mount-set-state": 2,
|
||||
"react/no-did-update-set-state": 2,
|
||||
"react/no-multi-comp": [ 2, { "ignoreStateless": true } ],
|
||||
"react/no-unknown-property": 2,
|
||||
"react/prop-types": 2,
|
||||
"react/react-in-jsx-scope": 2,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/sort-prop-types": 2,
|
||||
|
||||
"import/default": 2,
|
||||
"import/export": 2,
|
||||
"import/extensions": [ 0, "always" ],
|
||||
"import/first": 2,
|
||||
"import/named": 2,
|
||||
"import/namespace": 2,
|
||||
"import/newline-after-import": 2,
|
||||
"import/no-duplicates": 2,
|
||||
"import/no-unresolved": 2,
|
||||
"import/unambiguous": 2,
|
||||
|
||||
"prefer-object-spread/prefer-object-spread": 2
|
||||
}
|
||||
"extends": "freecodecamp"
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ function formatFields({ type, ...fields }) {
|
||||
}, { type });
|
||||
}
|
||||
|
||||
export default function analyticsSaga(actions, { getState }, { window }) {
|
||||
export default function analyticsSaga(actions, _, { window }) {
|
||||
const { ga } = window;
|
||||
if (typeof ga !== 'function') {
|
||||
console.log('GA not found');
|
||||
|
@ -7,20 +7,21 @@ import { removeCodeUri, getCodeUri } from '../utils/code-uri';
|
||||
import { setContent } from '../../common/utils/polyvinyl';
|
||||
|
||||
import {
|
||||
types as app,
|
||||
userSelector,
|
||||
challengeSelector
|
||||
} from '../../common/app/redux';
|
||||
import { makeToast } from '../../common/app/Toasts/redux';
|
||||
import {
|
||||
types,
|
||||
savedCodeFound,
|
||||
updateMain,
|
||||
lockUntrustedCode,
|
||||
|
||||
keySelector,
|
||||
filesSelector,
|
||||
codeLockedSelector
|
||||
} from '../../common/app/routes/challenges/redux';
|
||||
} from '../../common/app/routes/Challenges/redux';
|
||||
|
||||
import { filesSelector, savedCodeFound } from '../../common/app/files';
|
||||
|
||||
const legacyPrefixes = [
|
||||
'Bonfire: ',
|
||||
@ -59,18 +60,19 @@ function legacyToFile(code, files, key) {
|
||||
}
|
||||
|
||||
export function clearCodeEpic(actions, { getState }) {
|
||||
return actions::ofType(types.clearSavedCode)
|
||||
.map(() => {
|
||||
return actions::ofType(types.submitChallenge.complete)
|
||||
.do(() => {
|
||||
const { id } = challengeSelector(getState());
|
||||
store.remove(id);
|
||||
})
|
||||
.ignoreElements();
|
||||
}
|
||||
|
||||
export function saveCodeEpic(actions, { getState }) {
|
||||
return actions::ofType(types.saveCode)
|
||||
return actions::ofType(types.executeChallenge)
|
||||
// do not save challenge if code is locked
|
||||
.filter(() => !codeLockedSelector(getState()))
|
||||
.map(() => {
|
||||
.do(() => {
|
||||
const { id } = challengeSelector(getState());
|
||||
const files = filesSelector(getState());
|
||||
store.set(id, files);
|
||||
@ -79,7 +81,11 @@ export function saveCodeEpic(actions, { getState }) {
|
||||
}
|
||||
|
||||
export function loadCodeEpic(actions, { getState }, { window, location }) {
|
||||
return actions::ofType(types.loadCode)
|
||||
return Observable.merge(
|
||||
actions::ofType(app.appMounted),
|
||||
actions::ofType(types.onRouteChallenges)
|
||||
.distinctUntilChanged(({ payload: { dashedName } }) => dashedName)
|
||||
)
|
||||
.flatMap(() => {
|
||||
let finalFiles;
|
||||
const state = getState();
|
||||
|
@ -17,11 +17,11 @@ import {
|
||||
frameMain,
|
||||
frameTests,
|
||||
initOutput,
|
||||
saveCode,
|
||||
|
||||
filesSelector,
|
||||
codeLockedSelector
|
||||
} from '../../common/app/routes/challenges/redux';
|
||||
} from '../../common/app/routes/Challenges/redux';
|
||||
|
||||
import { filesSelector } from '../../common/app/files';
|
||||
|
||||
export default function executeChallengeEpic(actions, { getState }) {
|
||||
return actions::ofType(types.executeChallenge, types.updateMain)
|
||||
@ -47,7 +47,7 @@ export default function executeChallengeEpic(actions, { getState }) {
|
||||
frameMain(payload)
|
||||
];
|
||||
if (type === types.executeChallenge) {
|
||||
actions.push(saveCode(), frameTests(payload));
|
||||
actions.push(frameTests(payload));
|
||||
}
|
||||
return Observable.from(actions, null, null, Scheduler.default);
|
||||
})
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
|
||||
codeLockedSelector,
|
||||
testsSelector
|
||||
} from '../../common/app/routes/challenges/redux';
|
||||
} from '../../common/app/routes/Challenges/redux';
|
||||
|
||||
// we use two different frames to make them all essentially pure functions
|
||||
// main iframe is responsible rendering the preview and is where we proxy the
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { types } from '../../common/app/redux';
|
||||
import { ofType } from 'redux-epic';
|
||||
|
||||
export default function hardGoToSaga(actions, { getState }, { history }) {
|
||||
export default function hardGoToSaga(actions, _, { history }) {
|
||||
return actions::ofType(types.hardGoTo)
|
||||
.map(({ payload = '/settings' }) => {
|
||||
history.pushState(history.state, null, payload);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Observable } from 'rx';
|
||||
import MouseTrap from 'mousetrap';
|
||||
import { push } from 'react-router-redux';
|
||||
import { push } from 'redux-first-router';
|
||||
import {
|
||||
toggleNightMode,
|
||||
hardGoTo
|
||||
|
@ -1,21 +1,13 @@
|
||||
import './es6-shims';
|
||||
import Rx from 'rx';
|
||||
import React from 'react';
|
||||
import debug from 'debug';
|
||||
import { Router } from 'react-router';
|
||||
import {
|
||||
routerMiddleware,
|
||||
routerReducer as routing,
|
||||
syncHistoryWithStore
|
||||
} from 'react-router-redux';
|
||||
import { render } from 'redux-epic';
|
||||
import { createHistory } from 'history';
|
||||
import createHistory from 'history/createBrowserHistory';
|
||||
import useLangRoutes from './utils/use-lang-routes';
|
||||
import sendPageAnalytics from './utils/send-page-analytics';
|
||||
import flashToToast from './utils/flash-to-toast';
|
||||
|
||||
import createApp from '../common/app';
|
||||
import provideStore from '../common/app/provide-store';
|
||||
import { App, createApp, provideStore } from '../common/app';
|
||||
import { getLangFromPath } from '../common/app/utils/lang';
|
||||
|
||||
// client specific epics
|
||||
@ -32,13 +24,13 @@ const log = debug('fcc:client');
|
||||
const hotReloadTimeout = 2000;
|
||||
const { csrf: { token: csrfToken } = {} } = window.__fcc__;
|
||||
const DOMContainer = document.getElementById('fcc');
|
||||
const initialState = isColdStored() ?
|
||||
const defaultState = isColdStored() ?
|
||||
getColdStorage() :
|
||||
window.__fcc__.data;
|
||||
const primaryLang = getLangFromPath(window.location.pathname);
|
||||
|
||||
initialState.app.csrfToken = csrfToken;
|
||||
initialState.toasts = flashToToast(window.__fcc__.flash);
|
||||
defaultState.app.csrfToken = csrfToken;
|
||||
defaultState.toasts = flashToToast(window.__fcc__.flash);
|
||||
|
||||
// make empty object so hot reload works
|
||||
window.__fcc__ = {};
|
||||
@ -49,7 +41,6 @@ const history = useLangRoutes(createHistory, primaryLang)();
|
||||
sendPageAnalytics(history, window.ga);
|
||||
|
||||
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
|
||||
const adjustUrlOnReplay = !!window.devToolsExtension;
|
||||
|
||||
const epicOptions = {
|
||||
isDev,
|
||||
@ -61,14 +52,10 @@ const epicOptions = {
|
||||
|
||||
createApp({
|
||||
history,
|
||||
syncHistoryWithStore,
|
||||
syncOptions: { adjustUrlOnReplay },
|
||||
serviceOptions,
|
||||
initialState,
|
||||
middlewares: [ routerMiddleware(history) ],
|
||||
defaultState,
|
||||
epics,
|
||||
epicOptions,
|
||||
reducers: { routing },
|
||||
enhancers: [ devTools ]
|
||||
})
|
||||
.doOnNext(({ store }) => {
|
||||
@ -83,13 +70,7 @@ createApp({
|
||||
}
|
||||
})
|
||||
.doOnNext(() => log('rendering'))
|
||||
.flatMap(
|
||||
({ props, store }) => render(
|
||||
provideStore(React.createElement(Router, props), store),
|
||||
DOMContainer
|
||||
),
|
||||
({ store }) => store
|
||||
)
|
||||
.flatMap(({ store }) => render(provideStore(App, store), DOMContainer))
|
||||
.subscribe(
|
||||
() => debug('react rendered'),
|
||||
err => { throw err; },
|
||||
|
@ -1,29 +1,34 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ns from './ns.json';
|
||||
import {
|
||||
appMounted,
|
||||
fetchUser,
|
||||
updateAppLang,
|
||||
|
||||
userSelector
|
||||
isSignedInSelector
|
||||
} from './redux';
|
||||
|
||||
import Nav from './Nav';
|
||||
import Toasts from './Toasts';
|
||||
import NotFound from './NotFound';
|
||||
import { mainRouteSelector } from './routes/redux';
|
||||
import Challenges from './routes/Challenges';
|
||||
import Settings from './routes/Settings';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
appMounted,
|
||||
fetchUser,
|
||||
updateAppLang
|
||||
fetchUser
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { username } = userSelector(state);
|
||||
const isSignedIn = isSignedInSelector(state);
|
||||
const route = mainRouteSelector(state);
|
||||
return {
|
||||
toast: state.app.toast,
|
||||
isSignedIn: !!username
|
||||
isSignedIn,
|
||||
route
|
||||
};
|
||||
};
|
||||
|
||||
@ -32,19 +37,17 @@ const propTypes = {
|
||||
children: PropTypes.node,
|
||||
fetchUser: PropTypes.func,
|
||||
isSignedIn: PropTypes.bool,
|
||||
params: PropTypes.object,
|
||||
toast: PropTypes.object,
|
||||
updateAppLang: PropTypes.func.isRequired
|
||||
route: PropTypes.string,
|
||||
toast: PropTypes.object
|
||||
};
|
||||
|
||||
const routes = {
|
||||
challenges: Challenges,
|
||||
settings: Settings
|
||||
};
|
||||
|
||||
// export plain class for testing
|
||||
export class FreeCodeCamp extends React.Component {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.params.lang !== nextProps.params.lang) {
|
||||
this.props.updateAppLang(nextProps.params.lang);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.appMounted();
|
||||
if (!this.props.isSignedIn) {
|
||||
@ -53,14 +56,18 @@ export class FreeCodeCamp extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
route
|
||||
} = this.props;
|
||||
const Child = routes[route] || NotFound;
|
||||
// we render nav after the content
|
||||
// to allow the panes to update
|
||||
// redux store, which will update the bin
|
||||
// buttons in the nav
|
||||
return (
|
||||
<div className={ `${ns}-container` }>
|
||||
{ this.props.children }
|
||||
<Nav />
|
||||
<Child />
|
||||
<Toasts />
|
||||
</div>
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import ns from './ns.json';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FA from 'react-fontawesome';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Link } from 'react-router';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import classnames from 'classnames';
|
||||
import debug from 'debug';
|
||||
@ -13,6 +13,8 @@ import {
|
||||
} from './redux';
|
||||
import { userSelector } from '../redux';
|
||||
import { challengeMapSelector } from '../entities';
|
||||
import { Link } from '../Router';
|
||||
import { onRouteChallenges } from '../routes/Challenges/redux';
|
||||
|
||||
const propTypes = {
|
||||
block: PropTypes.string,
|
||||
@ -27,15 +29,7 @@ const propTypes = {
|
||||
isRequired: PropTypes.bool,
|
||||
title: PropTypes.string
|
||||
};
|
||||
function mapDispatchToProps(dispatch, { dashedName }) {
|
||||
const dispatchers = {
|
||||
clickOnChallenge: e => {
|
||||
e.preventDefault();
|
||||
return dispatch(clickOnChallenge(dashedName));
|
||||
}
|
||||
};
|
||||
return () => dispatchers;
|
||||
}
|
||||
const mapDispatchToProps = { clickOnChallenge };
|
||||
|
||||
function makeMapStateToProps(_, { dashedName }) {
|
||||
return createSelector(
|
||||
@ -152,8 +146,11 @@ export class Challenge extends PureComponent {
|
||||
className={ challengeClassName }
|
||||
key={ title }
|
||||
>
|
||||
<Link to={ `/challenges/${block}/${dashedName}` }>
|
||||
<span onClick={ clickOnChallenge }>
|
||||
<Link
|
||||
onClick={ clickOnChallenge }
|
||||
to={ onRouteChallenges({ dashedName, block }) }
|
||||
>
|
||||
<span >
|
||||
{ title }
|
||||
{ this.renderCompleted(isCompleted, isLocked) }
|
||||
{ this.renderRequired(isRequired) }
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { Col, Row } from 'react-bootstrap';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { createTypes } from 'redux-create-types';
|
||||
import { createAction, handleActions } from 'redux-actions';
|
||||
import {
|
||||
createAction,
|
||||
createTypes,
|
||||
handleActions
|
||||
} from 'berkeleys-redux-utils';
|
||||
import { createSelector } from 'reselect';
|
||||
import identity from 'lodash/identity';
|
||||
import noop from 'lodash/noop';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
|
||||
import selectChallengeEpic from './select-challenge-epic.js';
|
||||
|
||||
import * as utils from './utils.js';
|
||||
import ns from '../ns.json';
|
||||
import {
|
||||
@ -13,11 +14,10 @@ import {
|
||||
createEventMetaCreator
|
||||
} from '../../redux';
|
||||
|
||||
export const epics = [
|
||||
selectChallengeEpic
|
||||
];
|
||||
export const epics = [];
|
||||
|
||||
export const types = createTypes([
|
||||
'onRouteMap',
|
||||
'initMap',
|
||||
|
||||
'toggleThisPanel',
|
||||
@ -37,7 +37,7 @@ export const collapseAll = createAction(types.collapseAll);
|
||||
export const expandAll = createAction(types.expandAll);
|
||||
export const clickOnChallenge = createAction(
|
||||
types.clickOnChallenge,
|
||||
identity,
|
||||
noop,
|
||||
createEventMetaCreator({
|
||||
category: capitalize(ns),
|
||||
action: 'click',
|
||||
@ -88,42 +88,38 @@ export function makePanelHiddenSelector(name) {
|
||||
// }]
|
||||
// }
|
||||
// }
|
||||
export default function createReducer() {
|
||||
const reducer = 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
|
||||
};
|
||||
},
|
||||
[app.fetchChallenges.complete]: (state, { payload }) => {
|
||||
const { entities, result } = payload;
|
||||
return {
|
||||
...state,
|
||||
mapUi: utils.createMapUi(entities, result)
|
||||
};
|
||||
}
|
||||
export default handleActions(
|
||||
()=> ({
|
||||
[types.toggleThisPanel]: (state, { payload: name }) => {
|
||||
return {
|
||||
...state,
|
||||
mapUi: utils.toggleThisPanel(state.mapUi, name)
|
||||
};
|
||||
},
|
||||
initialState
|
||||
);
|
||||
|
||||
reducer.toString = () => ns;
|
||||
return reducer;
|
||||
}
|
||||
[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
|
||||
};
|
||||
},
|
||||
[app.fetchChallenges.complete]: (state, { payload }) => {
|
||||
const { entities, result } = payload;
|
||||
return {
|
||||
...state,
|
||||
mapUi: utils.createMapUi(entities, result)
|
||||
};
|
||||
}
|
||||
}),
|
||||
initialState,
|
||||
ns
|
||||
);
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { ofType } from 'redux-epic';
|
||||
|
||||
import { types } from './';
|
||||
import { updateCurrentChallenge } from '../../redux';
|
||||
|
||||
export default function selectChallengeEpic(actions) {
|
||||
return actions::ofType(types.clickOnChallenge)
|
||||
.pluck('payload')
|
||||
.map(updateCurrentChallenge);
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { NavItem } from 'react-bootstrap';
|
||||
|
||||
const propTypes = {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import {
|
||||
MenuItem,
|
||||
Nav,
|
||||
@ -14,6 +14,7 @@ import {
|
||||
NavbarBrand
|
||||
} from 'react-bootstrap';
|
||||
|
||||
import { Link } from '../Router';
|
||||
import navLinks from './links.json';
|
||||
import SignUp from './Sign-Up.jsx';
|
||||
import BinButton from './Bin-Button.jsx';
|
||||
@ -28,35 +29,36 @@ import {
|
||||
} from './redux';
|
||||
import {
|
||||
userSelector,
|
||||
isSignedInSelector,
|
||||
signInLoadingSelector
|
||||
} from '../redux';
|
||||
import { nameToTypeSelector, panesSelector } from '../Panes/redux';
|
||||
import { panesSelector } from '../Panes/redux';
|
||||
|
||||
|
||||
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
userSelector,
|
||||
isSignedInSelector,
|
||||
dropdownSelector,
|
||||
signInLoadingSelector,
|
||||
panesSelector,
|
||||
nameToTypeSelector,
|
||||
(
|
||||
{ username, picture, points },
|
||||
isSignedIn,
|
||||
isDropdownOpen,
|
||||
showLoading,
|
||||
panes,
|
||||
nameToType
|
||||
) => {
|
||||
return {
|
||||
panes: panes.map(name => {
|
||||
panes: panes.map(({ name, type }) => {
|
||||
return {
|
||||
content: name,
|
||||
action: nameToType[name]
|
||||
action: type
|
||||
};
|
||||
}, {}),
|
||||
isDropdownOpen,
|
||||
isSignedIn: !!username,
|
||||
isSignedIn,
|
||||
picture,
|
||||
points,
|
||||
showLoading,
|
||||
@ -104,25 +106,19 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
|
||||
};
|
||||
}
|
||||
|
||||
const propTypes = navLinks.reduce(
|
||||
(pt, { content }) => {
|
||||
const handler = `handle${capitalize(content)}Click`;
|
||||
pt[handler] = PropTypes.func.isRequired;
|
||||
return pt;
|
||||
},
|
||||
{
|
||||
panes: PropTypes.array,
|
||||
clickOnLogo: PropTypes.func.isRequired,
|
||||
closeDropdown: PropTypes.func.isRequired,
|
||||
isDropdownOpen: PropTypes.bool,
|
||||
openDropdown: PropTypes.func.isRequired,
|
||||
picture: PropTypes.string,
|
||||
points: PropTypes.number,
|
||||
showLoading: PropTypes.bool,
|
||||
signedIn: PropTypes.bool,
|
||||
username: PropTypes.string
|
||||
}
|
||||
);
|
||||
const propTypes = {
|
||||
clickOnLogo: PropTypes.func.isRequired,
|
||||
clickOnMap: PropTypes.func.isRequired,
|
||||
closeDropdown: PropTypes.func.isRequired,
|
||||
isDropdownOpen: PropTypes.bool,
|
||||
openDropdown: PropTypes.func.isRequired,
|
||||
panes: PropTypes.array,
|
||||
picture: PropTypes.string,
|
||||
points: PropTypes.number,
|
||||
showLoading: PropTypes.bool,
|
||||
signedIn: PropTypes.bool,
|
||||
username: PropTypes.string
|
||||
};
|
||||
|
||||
export class FCCNav extends React.Component {
|
||||
renderLink(isNavItem, { isReact, isDropdown, content, link, links, target }) {
|
||||
@ -154,7 +150,7 @@ export class FCCNav extends React.Component {
|
||||
}
|
||||
if (isReact) {
|
||||
return (
|
||||
<LinkContainer
|
||||
<Link
|
||||
key={ content }
|
||||
onClick={ this.props[`handle${content}Click`] }
|
||||
to={ link }
|
||||
@ -164,7 +160,7 @@ export class FCCNav extends React.Component {
|
||||
>
|
||||
{ content }
|
||||
</Component>
|
||||
</LinkContainer>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
@ -1,7 +1,10 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { NavItem } from 'react-bootstrap';
|
||||
|
||||
import { Link } from '../Router';
|
||||
import { onRouteSettings } from '../routes/Settings/redux';
|
||||
|
||||
// this is separated out to prevent react bootstrap's
|
||||
// NavBar from injecting unknown props to the li component
|
||||
|
||||
@ -36,7 +39,7 @@ export default function SignUpButton({
|
||||
className='nav-avatar'
|
||||
key='user'
|
||||
>
|
||||
<Link to='/settings'>
|
||||
<Link to={ onRouteSettings() }>
|
||||
<span className='nav-username hidden-sm'> { username } </span>
|
||||
<span className='nav-points'> [ { points || 1 } ] </span>
|
||||
<span className='nav-picture-container'>
|
||||
|
@ -1,7 +1,10 @@
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { createTypes } from 'redux-create-types';
|
||||
import { createAction, handleActions } from 'redux-actions';
|
||||
import noop from 'lodash/noop';
|
||||
import {
|
||||
createAction,
|
||||
createTypes,
|
||||
handleActions
|
||||
} from 'berkeleys-redux-utils';
|
||||
|
||||
import loadCurrentChallengeEpic from './load-current-challenge-epic.js';
|
||||
import binEpic from './bin-epic.js';
|
||||
@ -62,8 +65,8 @@ const initialState = {
|
||||
|
||||
export const dropdownSelector = state => state[ns].isDropdownOpen;
|
||||
|
||||
export default function createReducer() {
|
||||
const reducer = handleActions({
|
||||
export default handleActions(
|
||||
() => ({
|
||||
[types.closeDropdown]: state => ({
|
||||
...state,
|
||||
isDropdownOpen: false
|
||||
@ -72,8 +75,7 @@ export default function createReducer() {
|
||||
...state,
|
||||
isDropdownOpen: true
|
||||
})
|
||||
}, initialState);
|
||||
|
||||
reducer.toString = () => ns;
|
||||
return reducer;
|
||||
}
|
||||
}),
|
||||
initialState,
|
||||
ns
|
||||
);
|
||||
|
@ -2,12 +2,11 @@ import { ofType } from 'redux-epic';
|
||||
|
||||
import { types } from './';
|
||||
import {
|
||||
updateCurrentChallenge,
|
||||
|
||||
userSelector,
|
||||
firstChallengeSelector,
|
||||
challengeSelector
|
||||
} from '../../redux';
|
||||
import { onRouteChallenges } from '../../routes/Challenges/redux';
|
||||
import { entitiesSelector } from '../../entities';
|
||||
|
||||
export default function loadCurrentChallengeEpic(actions, { getState }) {
|
||||
@ -55,7 +54,5 @@ export default function loadCurrentChallengeEpic(actions, { getState }) {
|
||||
// don't reload if the challenge is already loaded.
|
||||
// This may change to toast to avoid user confusion
|
||||
))
|
||||
.map(({ finalChallenge }) => {
|
||||
return updateCurrentChallenge(finalChallenge.dashedName);
|
||||
});
|
||||
.map(({ finalChallenge }) => onRouteChallenges(finalChallenge));
|
||||
}
|
||||
|
@ -1,23 +1,15 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hardGoTo } from '../redux';
|
||||
import React from 'react';
|
||||
|
||||
const propTypes = {
|
||||
hardGoTo: PropTypes.func,
|
||||
location: PropTypes.object
|
||||
};
|
||||
// import PropTypes from 'prop-types';
|
||||
|
||||
export class NotFound extends React.Component {
|
||||
componentWillMount() {
|
||||
this.props.hardGoTo(this.props.location.pathname);
|
||||
}
|
||||
const propTypes = {};
|
||||
|
||||
render() {
|
||||
return <span />;
|
||||
}
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div>404 Not Found</div>
|
||||
);
|
||||
}
|
||||
|
||||
NotFound.displayName = 'NotFound';
|
||||
NotFound.propTypes = propTypes;
|
||||
|
||||
export default connect(null, { hardGoTo })(NotFound);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { dividerClicked } from './redux';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const mapStateToProps = null;
|
||||
|
@ -1,48 +1,24 @@
|
||||
import React, { PureComponent, PropTypes } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Panes from './Panes.jsx';
|
||||
import {
|
||||
panesMounted,
|
||||
panesUpdated,
|
||||
panesWillMount,
|
||||
panesWillUnmount
|
||||
} from './redux';
|
||||
import { panesMounted } from './redux';
|
||||
|
||||
const mapStateToProps = null;
|
||||
const mapDispatchToProps = {
|
||||
panesMounted,
|
||||
panesUpdated,
|
||||
panesWillMount,
|
||||
panesWillUnmount
|
||||
panesMounted
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
nameToComponent: PropTypes.object.isRequired,
|
||||
panesMounted: PropTypes.func.isRequired,
|
||||
panesUpdated: PropTypes.func.isRequired,
|
||||
panesWillMount: PropTypes.func.isRequired,
|
||||
panesWillUnmount: PropTypes.func.isRequired
|
||||
panesMounted: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export class PanesContainer extends PureComponent {
|
||||
componentWillMount() {
|
||||
this.props.panesWillMount(Object.keys(this.props.nameToComponent));
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.panesMounted();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.panesWillUnmount();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.nameToComponent !== this.props.nameToComponent) {
|
||||
this.props.panesUpdated(Object.keys(nextProps.nameToComponent));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Panes { ...this.props } />
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PureComponent, PropTypes } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
@ -20,7 +21,7 @@ const mapStateToProps = createSelector(
|
||||
let lastDividerPosition = 0;
|
||||
return {
|
||||
panes: panes
|
||||
.map(name => panesByName[name])
|
||||
.map(({ name }) => panesByName[name])
|
||||
.filter(({ isHidden })=> !isHidden)
|
||||
.map((pane, index, { length: numOfPanes }) => {
|
||||
const dividerLeft = pane.dividerLeft || 0;
|
||||
|
@ -1,11 +1,18 @@
|
||||
import { combineActions, createAction, handleActions } from 'redux-actions';
|
||||
import { createTypes } from 'redux-create-types';
|
||||
import clamp from 'lodash/clamp';
|
||||
import { isLocationAction } from 'redux-first-router';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
composeReducers,
|
||||
createAction,
|
||||
createTypes,
|
||||
handleActions
|
||||
} from 'berkeleys-redux-utils';
|
||||
|
||||
import ns from '../ns.json';
|
||||
|
||||
import windowEpic from './window-epic.js';
|
||||
import dividerEpic from './divider-epic.js';
|
||||
import { challengeMetaSelector } from '../../routes/Challenges/redux';
|
||||
import { types as app } from '../../redux';
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
export const epics = [
|
||||
@ -14,6 +21,7 @@ export const epics = [
|
||||
];
|
||||
|
||||
export const types = createTypes([
|
||||
'panesUpdatedThroughFetch',
|
||||
'panesMounted',
|
||||
'panesUpdated',
|
||||
'panesWillMount',
|
||||
@ -30,6 +38,11 @@ export const types = createTypes([
|
||||
'hidePane'
|
||||
], ns);
|
||||
|
||||
export const panesUpdatedThroughFetch = createAction(
|
||||
types.panesUpdatedThroughFetch,
|
||||
null,
|
||||
panesView => ({ panesView })
|
||||
);
|
||||
export const panesMounted = createAction(types.panesMounted);
|
||||
export const panesUpdated = createAction(types.panesUpdated);
|
||||
export const panesWillMount = createAction(types.panesWillMount);
|
||||
@ -78,9 +91,45 @@ function getDividerLeft(numOfPanes, index) {
|
||||
return dividerLeft;
|
||||
}
|
||||
|
||||
export default function createPanesAspects(typeToName) {
|
||||
function forEachConfig(config, cb) {
|
||||
return _.forEach(config, (val, key) => {
|
||||
// val is a sub config
|
||||
if (_.isObject(val) && !val.name) {
|
||||
return forEachConfig(val, cb);
|
||||
}
|
||||
return cb(config, key);
|
||||
});
|
||||
}
|
||||
|
||||
function reduceConfig(config, cb, acc = {}) {
|
||||
return _.reduce(config, (acc, val, key) => {
|
||||
if (_.isObject(val) && !val.name) {
|
||||
return reduceConfig(val, cb, acc);
|
||||
}
|
||||
return cb(acc, val, key);
|
||||
}, acc);
|
||||
}
|
||||
|
||||
const getPaneName = (panes, index) => (panes[index] || {}).name || '';
|
||||
|
||||
export const createPaneMap = (ns, getPanesMap) => {
|
||||
const panesMap = _.reduce(getPanesMap(), (map, val, key) => {
|
||||
let paneConfig = val;
|
||||
if (typeof val === 'string') {
|
||||
paneConfig = {
|
||||
name: val
|
||||
};
|
||||
}
|
||||
map[key] = paneConfig;
|
||||
return map;
|
||||
}, {});
|
||||
return Object.defineProperty(panesMap, 'toString', { value: () => ns });
|
||||
};
|
||||
|
||||
|
||||
export default function createPanesAspects(config) {
|
||||
if (isDev) {
|
||||
Object.keys(typeToName).forEach(actionType => {
|
||||
forEachConfig(config, (typeToName, actionType) => {
|
||||
if (actionType === 'undefined') {
|
||||
throw new Error(
|
||||
`action type for ${typeToName[actionType]} is undefined`
|
||||
@ -88,18 +137,24 @@ export default function createPanesAspects(typeToName) {
|
||||
}
|
||||
});
|
||||
}
|
||||
const nameToType = Object.keys(typeToName).reduce((map, type) => {
|
||||
map[typeToName[type]] = type;
|
||||
return map;
|
||||
}, {});
|
||||
function getInitialState() {
|
||||
return {
|
||||
...initialState,
|
||||
nameToType
|
||||
};
|
||||
}
|
||||
const typeToName = reduceConfig(config, (acc, val, type) => {
|
||||
const name = _.isObject(val) ? val.name : val;
|
||||
acc[type] = name;
|
||||
return acc;
|
||||
});
|
||||
|
||||
function middleware() {
|
||||
function middleware({ getState }) {
|
||||
const filterPanes = panesMap => _.reduce(panesMap, (panes, pane, type) => {
|
||||
if (typeof pane.filter !== 'function' || pane.filter(getState())) {
|
||||
panes[type] = pane;
|
||||
}
|
||||
return panes;
|
||||
}, {});
|
||||
// we cache the previous map so that we can attach it to the fetchChallenge
|
||||
let previousMap;
|
||||
// show panes on challenge route
|
||||
// select panes map on viewType (this is state dependent)
|
||||
// filter panes out on state
|
||||
return next => action => {
|
||||
let finalAction = action;
|
||||
if (isPanesAction(action, typeToName)) {
|
||||
@ -107,130 +162,150 @@ export default function createPanesAspects(typeToName) {
|
||||
...action,
|
||||
meta: {
|
||||
...action.meta,
|
||||
isPaneAction: true
|
||||
isPaneAction: true,
|
||||
paneName: typeToName[action.type]
|
||||
}
|
||||
};
|
||||
}
|
||||
return next(finalAction);
|
||||
const result = next(finalAction);
|
||||
if (isLocationAction(action)) {
|
||||
// location matches a panes route
|
||||
if (config[action.type]) {
|
||||
const paneMap = previousMap = config[action.type];
|
||||
const meta = challengeMetaSelector(getState());
|
||||
const viewMap = paneMap[meta.viewType] || {};
|
||||
next(panesUpdatedThroughFetch(filterPanes(viewMap)));
|
||||
} else {
|
||||
next(panesUpdatedThroughFetch({}));
|
||||
}
|
||||
}
|
||||
if (action.type === app.fetchChallenge.complete) {
|
||||
const meta = challengeMetaSelector(getState());
|
||||
const viewMap = previousMap[meta.viewType] || {};
|
||||
next(panesUpdatedThroughFetch(filterPanes(viewMap)));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
const reducer = handleActions({
|
||||
[types.dividerClicked]: (state, { payload: name }) => ({
|
||||
...state,
|
||||
pressedDivider: name
|
||||
}),
|
||||
[types.dividerMoved]: (state, { payload: clientX }) => {
|
||||
const { width, pressedDivider: paneName } = state;
|
||||
const dividerBuffer = (200 / width) * 100;
|
||||
const paneIndex = state.panes.indexOf(paneName);
|
||||
const currentPane = state.panesByName[paneName];
|
||||
const rightPane = state.panesByName[state.panes[paneIndex + 1]] || {};
|
||||
const leftPane = state.panesByName[state.panes[paneIndex - 1]] || {};
|
||||
const rightBound = (rightPane.dividerLeft || 100) - dividerBuffer;
|
||||
const leftBound = (leftPane.dividerLeft || 0) + dividerBuffer;
|
||||
const newPosition = clamp(
|
||||
(clientX / width) * 100,
|
||||
leftBound,
|
||||
rightBound
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
panesByName: {
|
||||
...state.panesByName,
|
||||
[currentPane.name]: {
|
||||
...currentPane,
|
||||
dividerLeft: newPosition
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[types.mouseReleased]: state => ({ ...state, pressedDivider: null }),
|
||||
[types.windowResized]: (state, { payload: { height, width } }) => ({
|
||||
...state,
|
||||
height,
|
||||
width
|
||||
}),
|
||||
// used to clear bin buttons
|
||||
[types.panesWillUnmount]: state => ({
|
||||
...state,
|
||||
panes: [],
|
||||
panesByName: {},
|
||||
pressedDivider: null
|
||||
}),
|
||||
[
|
||||
combineActions(
|
||||
panesWillMount,
|
||||
panesUpdated
|
||||
)
|
||||
]: (state, { payload: panes }) => {
|
||||
const numOfPanes = panes.length;
|
||||
return {
|
||||
...state,
|
||||
panes,
|
||||
panesByName: panes.reduce((panes, name, index) => {
|
||||
const dividerLeft = getDividerLeft(numOfPanes, index);
|
||||
panes[name] = {
|
||||
name,
|
||||
dividerLeft,
|
||||
isHidden: false
|
||||
};
|
||||
return panes;
|
||||
}, {})
|
||||
};
|
||||
},
|
||||
[types.updateNavHeight]: (state, { payload: navHeight }) => ({
|
||||
...state,
|
||||
navHeight
|
||||
})
|
||||
}, getInitialState());
|
||||
function metaReducer(state = getInitialState(), action) {
|
||||
if (action.meta && action.meta.isPaneAction) {
|
||||
const name = typeToName[action.type];
|
||||
const oldPane = state.panesByName[name];
|
||||
const pane = {
|
||||
...oldPane,
|
||||
isHidden: !oldPane.isHidden
|
||||
};
|
||||
const panesByName = {
|
||||
...state.panesByName,
|
||||
[name]: pane
|
||||
};
|
||||
const numOfPanes = state.panes.reduce((sum, name) => {
|
||||
return panesByName[name].isHidden ? sum : sum + 1;
|
||||
}, 0);
|
||||
let numOfHidden = 0;
|
||||
return {
|
||||
...state,
|
||||
panesByName: state.panes.reduce(
|
||||
(panesByName, name, index) => {
|
||||
if (!panesByName[name].isHidden) {
|
||||
const dividerLeft = getDividerLeft(
|
||||
numOfPanes,
|
||||
index - numOfHidden
|
||||
);
|
||||
panesByName[name] = {
|
||||
...panesByName[name],
|
||||
dividerLeft
|
||||
};
|
||||
} else {
|
||||
numOfHidden = numOfHidden + 1;
|
||||
const reducer = composeReducers(
|
||||
ns,
|
||||
handleActions(
|
||||
() => ({
|
||||
[types.dividerClicked]: (state, { payload: name }) => ({
|
||||
...state,
|
||||
pressedDivider: name
|
||||
}),
|
||||
[types.dividerMoved]: (state, { payload: clientX }) => {
|
||||
const { width, pressedDivider: paneName } = state;
|
||||
const dividerBuffer = (200 / width) * 100;
|
||||
const paneIndex =
|
||||
_.findIndex(state.panes, ({ name }) => paneName === name);
|
||||
const currentPane = state.panesByName[paneName];
|
||||
const rightPane =
|
||||
state.panesByName[getPaneName(state.panes, paneIndex + 1)] || {};
|
||||
const leftPane =
|
||||
state.panesByName[getPaneName(state.panes, paneIndex - 1)] || {};
|
||||
const rightBound = (rightPane.dividerLeft || 100) - dividerBuffer;
|
||||
const leftBound = (leftPane.dividerLeft || 0) + dividerBuffer;
|
||||
const newPosition = _.clamp(
|
||||
(clientX / width) * 100,
|
||||
leftBound,
|
||||
rightBound
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
panesByName: {
|
||||
...state.panesByName,
|
||||
[currentPane.name]: {
|
||||
...currentPane,
|
||||
dividerLeft: newPosition
|
||||
}
|
||||
}
|
||||
return panesByName;
|
||||
},
|
||||
panesByName
|
||||
)
|
||||
};
|
||||
};
|
||||
},
|
||||
[types.mouseReleased]: state => ({ ...state, pressedDivider: null }),
|
||||
[types.windowResized]: (state, { payload: { height, width } }) => ({
|
||||
...state,
|
||||
height,
|
||||
width
|
||||
}),
|
||||
// used to clear bin buttons
|
||||
[types.panesWillUnmount]: state => ({
|
||||
...state,
|
||||
panes: [],
|
||||
panesByName: {},
|
||||
pressedDivider: null
|
||||
}),
|
||||
[types.updateNavHeight]: (state, { payload: navHeight }) => ({
|
||||
...state,
|
||||
navHeight
|
||||
})
|
||||
}),
|
||||
initialState,
|
||||
),
|
||||
function metaReducer(state = initialState, action) {
|
||||
if (action.meta && action.meta.panesView) {
|
||||
const panesView = action.meta.panesView;
|
||||
const panes = _.map(panesView, ({ name }, type) => ({ name, type }));
|
||||
const numOfPanes = Object.keys(panes).length;
|
||||
return {
|
||||
...state,
|
||||
panes,
|
||||
panesByName: panes.reduce((panes, { name }, index) => {
|
||||
const dividerLeft = getDividerLeft(numOfPanes, index);
|
||||
panes[name] = {
|
||||
name,
|
||||
dividerLeft,
|
||||
isHidden: false
|
||||
};
|
||||
return panes;
|
||||
}, {})
|
||||
};
|
||||
}
|
||||
if (action.meta && action.meta.isPaneAction) {
|
||||
const name = action.meta.paneName;
|
||||
const oldPane = state.panesByName[name];
|
||||
const pane = {
|
||||
...oldPane,
|
||||
isHidden: !oldPane.isHidden
|
||||
};
|
||||
const panesByName = {
|
||||
...state.panesByName,
|
||||
[name]: pane
|
||||
};
|
||||
const numOfPanes = state.panes.reduce((sum, { name }) => {
|
||||
return panesByName[name].isHidden ? sum : sum + 1;
|
||||
}, 0);
|
||||
let numOfHidden = 0;
|
||||
return {
|
||||
...state,
|
||||
panesByName: state.panes.reduce(
|
||||
(panesByName, { name }, index) => {
|
||||
if (!panesByName[name].isHidden) {
|
||||
const dividerLeft = getDividerLeft(
|
||||
numOfPanes,
|
||||
index - numOfHidden
|
||||
);
|
||||
panesByName[name] = {
|
||||
...panesByName[name],
|
||||
dividerLeft
|
||||
};
|
||||
} else {
|
||||
numOfHidden = numOfHidden + 1;
|
||||
}
|
||||
return panesByName;
|
||||
},
|
||||
panesByName
|
||||
)
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
);
|
||||
|
||||
function finalReducer(state, action) {
|
||||
return reducer(metaReducer(state, action), action);
|
||||
}
|
||||
finalReducer.toString = () => ns;
|
||||
return {
|
||||
reducer: finalReducer,
|
||||
reducer,
|
||||
middleware
|
||||
};
|
||||
}
|
||||
|
80
common/app/Router/Link.jsx
Normal file
80
common/app/Router/Link.jsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectLocationState } from 'redux-first-router';
|
||||
|
||||
import toUrl from './to-url.js';
|
||||
import createHandler from './handle-press.js';
|
||||
import { langSelector } from './redux';
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.node,
|
||||
dispatch: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
redirect: PropTypes.bool,
|
||||
replace: PropTypes.bool,
|
||||
shouldDispatch: PropTypes.bool,
|
||||
target: PropTypes.string,
|
||||
to: PropTypes.oneOfType([ PropTypes.object, PropTypes.string ]).isRequired
|
||||
};
|
||||
|
||||
export const Link = (
|
||||
{
|
||||
children,
|
||||
dispatch,
|
||||
onClick,
|
||||
redirect,
|
||||
replace,
|
||||
shouldDispatch = true,
|
||||
target,
|
||||
to
|
||||
},
|
||||
{ store }
|
||||
) => {
|
||||
const state = store.getState();
|
||||
const lang = langSelector(state);
|
||||
const location = selectLocationState(state);
|
||||
const { routesMap } = location;
|
||||
const url = toUrl(to, routesMap, lang);
|
||||
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 }
|
||||
{ ...localProps }
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
Link.contextTypes = {
|
||||
store: PropTypes.object.isRequired
|
||||
};
|
||||
Link.propTypes = propTypes;
|
||||
|
||||
export default connect()(Link);
|
50
common/app/Router/handle-press.js
Normal file
50
common/app/Router/handle-press.js
Normal file
@ -0,0 +1,50 @@
|
||||
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
common/app/Router/index.js
Normal file
1
common/app/Router/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as Link } from './Link.jsx';
|
28
common/app/Router/redux/add-lang-enhancer.js
Normal file
28
common/app/Router/redux/add-lang-enhancer.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { langSelector } from './';
|
||||
|
||||
// This enhancers sole purpose is to add the lang prop to route actions so that
|
||||
// they do not need to be explicitally added when using a RFR route action.
|
||||
export default function addLangToRouteEnhancer(routesMap) {
|
||||
return createStore => (...args) => {
|
||||
const store = createStore(...args);
|
||||
|
||||
return {
|
||||
...store,
|
||||
dispatch(action) {
|
||||
if (
|
||||
routesMap[action.type] &&
|
||||
(action && action.payload && !action.payload.lang)
|
||||
) {
|
||||
action = {
|
||||
...action,
|
||||
payload: {
|
||||
...action.payload,
|
||||
lang: langSelector(store.getState()) || 'en'
|
||||
}
|
||||
};
|
||||
}
|
||||
return store.dispatch(action);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
5
common/app/Router/redux/index.js
Normal file
5
common/app/Router/redux/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { selectLocationState } from 'redux-first-router';
|
||||
|
||||
export const paramsSelector = state => selectLocationState(state).payload || {};
|
||||
export const langSelector = state => paramsSelector(state).lang || 'en';
|
||||
export const routesMapSelector = state => paramsSelector(state).routesMap || {};
|
45
common/app/Router/to-url.js
Normal file
45
common/app/Router/to-url.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { actionToPath, getOptions } from 'redux-first-router';
|
||||
|
||||
import { addLang } from '../utils/lang.js';
|
||||
|
||||
|
||||
export default (to, routesMap, lang = 'en') => {
|
||||
if (to && typeof to === 'string') {
|
||||
return addLang(to, lang);
|
||||
}
|
||||
|
||||
if (typeof to === 'object') {
|
||||
const { payload = {}, ...action } = to;
|
||||
|
||||
try {
|
||||
const { querySerializer } = getOptions();
|
||||
return actionToPath(
|
||||
{
|
||||
...action,
|
||||
payload: {
|
||||
...payload,
|
||||
lang: payload.lang || lang
|
||||
}
|
||||
},
|
||||
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,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { NotificationStack } from 'react-notification';
|
||||
@ -6,12 +7,12 @@ import { NotificationStack } from 'react-notification';
|
||||
import { removeToast } from './redux';
|
||||
import {
|
||||
submitChallenge,
|
||||
resetChallenge
|
||||
} from '../routes/challenges/redux';
|
||||
clickOnReset
|
||||
} from '../routes/Challenges/redux';
|
||||
|
||||
const registeredActions = {
|
||||
submitChallenge,
|
||||
resetChallenge
|
||||
clickOnReset
|
||||
};
|
||||
const mapStateToProps = state => ({ toasts: state.toasts });
|
||||
// we use styles here to overwrite those built into the library
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { createTypes } from 'redux-create-types';
|
||||
import { createAction, handleActions } from 'redux-actions';
|
||||
import {
|
||||
createAction,
|
||||
createTypes,
|
||||
handleActions
|
||||
} from 'berkeleys-redux-utils';
|
||||
|
||||
import ns from '../ns.json';
|
||||
|
||||
export const types = createTypes([
|
||||
'makeToast',
|
||||
'removeToast'
|
||||
], 'toast');
|
||||
], ns);
|
||||
|
||||
let key = 0;
|
||||
export const makeToast = createAction(
|
||||
@ -29,8 +32,8 @@ export const removeToast = createAction(
|
||||
|
||||
const initialState = [];
|
||||
|
||||
export default function createReducer() {
|
||||
const reducer = handleActions({
|
||||
export default handleActions(
|
||||
() => ({
|
||||
[types.makeToast]: (state, { payload: toast }) => [
|
||||
...state,
|
||||
toast
|
||||
@ -38,8 +41,7 @@ export default function createReducer() {
|
||||
[types.removeToast]: (state, { payload: key }) => state.filter(
|
||||
toast => toast.key !== key
|
||||
)
|
||||
}, initialState);
|
||||
|
||||
reducer.toString = () => ns;
|
||||
return reducer;
|
||||
}
|
||||
}),
|
||||
initialState,
|
||||
ns
|
||||
);
|
||||
|
@ -7,7 +7,7 @@
|
||||
// Here we invert the order in which
|
||||
// they are painted using css so the
|
||||
// nav is on top again
|
||||
.grid(@direction: column-reverse);
|
||||
.grid(@direction: column);
|
||||
}
|
||||
|
||||
.@{ns}-content {
|
||||
|
@ -1,42 +1,35 @@
|
||||
import { Observable } from 'rx';
|
||||
import { match } from 'react-router';
|
||||
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 createReducer from './create-reducer';
|
||||
import createRoutes from './create-routes.js';
|
||||
import appReducer from './reducer.js';
|
||||
import routesMap from './routes-map.js';
|
||||
import createPanesMap from './create-panes-map.js';
|
||||
import createPanesAspects from './Panes/redux';
|
||||
import addLangToRoutesEnhancer from './Router/redux/add-lang-enhancer.js';
|
||||
import epics from './epics';
|
||||
|
||||
import { onBeforeChange } from './utils/redux-first-router.js';
|
||||
import servicesCreator from '../utils/services-creator';
|
||||
|
||||
const createRouteProps = Observable.fromNodeCallback(match);
|
||||
|
||||
//
|
||||
// createApp(settings: {
|
||||
// location?: Location|String,
|
||||
// history?: History,
|
||||
// syncHistoryWithStore?: ((history, store) => history) = (x) => x,
|
||||
// initialState?: Object|Void,
|
||||
// defaultState?: Object|Void,
|
||||
// serviceOptions?: Object,
|
||||
// middlewares?: Function[],
|
||||
// sideReducers?: Object
|
||||
// enhancers?: Function[],
|
||||
// epics?: Function[],
|
||||
// }) => Observable
|
||||
//
|
||||
// Either location or history must be defined
|
||||
export default function createApp({
|
||||
location,
|
||||
history,
|
||||
syncHistoryWithStore = (x) => x,
|
||||
syncOptions = {},
|
||||
initialState,
|
||||
defaultState,
|
||||
serviceOptions = {},
|
||||
middlewares: sideMiddlewares = [],
|
||||
enhancers: sideEnhancers = [],
|
||||
reducers: sideReducers = {},
|
||||
epics: sideEpics = [],
|
||||
epicOptions: sideEpicOptions = {}
|
||||
}) {
|
||||
@ -50,12 +43,25 @@ export default function createApp({
|
||||
...epics,
|
||||
...sideEpics
|
||||
);
|
||||
|
||||
const {
|
||||
reducer: routesReducer,
|
||||
middleware: routesMiddleware,
|
||||
enhancer: routesEnhancer
|
||||
} = connectRoutes(history, routesMap, { onBeforeChange });
|
||||
|
||||
routesReducer.toString = () => 'location';
|
||||
|
||||
const {
|
||||
reducer: panesReducer,
|
||||
middleware: panesMiddleware
|
||||
} = createPanesAspects(createPanesMap());
|
||||
|
||||
const enhancer = compose(
|
||||
addLangToRoutesEnhancer(routesMap),
|
||||
routesEnhancer,
|
||||
applyMiddleware(
|
||||
routesMiddleware,
|
||||
panesMiddleware,
|
||||
epicMiddleware,
|
||||
...sideMiddlewares
|
||||
@ -64,33 +70,31 @@ export default function createApp({
|
||||
// on client side these are things like Redux DevTools
|
||||
...sideEnhancers
|
||||
);
|
||||
const reducer = createReducer(
|
||||
{
|
||||
[panesReducer]: panesReducer,
|
||||
...sideReducers
|
||||
},
|
||||
|
||||
const reducer = combineReducers(
|
||||
appReducer,
|
||||
panesReducer,
|
||||
routesReducer
|
||||
);
|
||||
|
||||
// create composed store enhancer
|
||||
// use store enhancer function to enhance `createStore` function
|
||||
// call enhanced createStore function with reducer and initialState
|
||||
// call enhanced createStore function with reducer and defaultState
|
||||
// to create store
|
||||
const store = createStore(reducer, initialState, enhancer);
|
||||
// sync history client side with store.
|
||||
// server side this is an identity function and history is undefined
|
||||
history = syncHistoryWithStore(history, store, syncOptions);
|
||||
const routes = createRoutes(store);
|
||||
// createRouteProps({
|
||||
// redirect: LocationDescriptor,
|
||||
// history: History,
|
||||
// routes: Object
|
||||
// }) => Observable
|
||||
return createRouteProps({ routes, location, history })
|
||||
.map(([ redirect, props ]) => ({
|
||||
redirect,
|
||||
props,
|
||||
reducer,
|
||||
store,
|
||||
epic: epicMiddleware
|
||||
}));
|
||||
const store = createStore(reducer, defaultState, enhancer);
|
||||
const location = selectLocationState(store.getState());
|
||||
|
||||
// ({
|
||||
// redirect,
|
||||
// props,
|
||||
// reducer,
|
||||
// store,
|
||||
// epic: epicMiddleware
|
||||
// }));
|
||||
return Observable.of({
|
||||
store,
|
||||
epic: epicMiddleware,
|
||||
location,
|
||||
notFound: false
|
||||
});
|
||||
}
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
|
||||
import app from './redux';
|
||||
import entities from './entities';
|
||||
import map from './Map/redux';
|
||||
import nav from './Nav/redux';
|
||||
import routes from './routes/redux';
|
||||
import toasts from './Toasts/redux';
|
||||
// not ideal but should go away once we move to react-redux-form
|
||||
import { projectNormalizer } from './routes/challenges/redux';
|
||||
|
||||
export default function createReducer(sideReducers) {
|
||||
// reducers exported from features need to be factories
|
||||
// this helps avoid cyclic requires messing up reducer creation
|
||||
// We end up with exports from files being undefined as node tries
|
||||
// to resolve cyclic dependencies.
|
||||
// This prevents that by wrapping the `handleActions` call so that the ref
|
||||
// to types imported from parent features are closures and can be resolved
|
||||
// by node before we need them.
|
||||
const reducerMap = [
|
||||
app,
|
||||
entities,
|
||||
map,
|
||||
nav,
|
||||
routes,
|
||||
toasts
|
||||
]
|
||||
.map(createReducer => createReducer())
|
||||
.reduce((arr, cur) => arr.concat(cur), [])
|
||||
.reduce(
|
||||
(reducerMap, reducer) => {
|
||||
reducerMap[reducer] = reducer;
|
||||
return reducerMap;
|
||||
},
|
||||
{
|
||||
form: formReducer.normalize({ ...projectNormalizer }),
|
||||
...sideReducers
|
||||
}
|
||||
);
|
||||
return combineReducers(reducerMap);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import App from './App.jsx';
|
||||
import createChildRoute from './routes';
|
||||
|
||||
export default function createRoutes(store) {
|
||||
return {
|
||||
components: App,
|
||||
...createChildRoute(store)
|
||||
};
|
||||
}
|
@ -1,23 +1,22 @@
|
||||
import { createTypes } from 'redux-create-types';
|
||||
import { createAction, handleActions } from 'redux-actions';
|
||||
import {
|
||||
composeReducers,
|
||||
createAction,
|
||||
createTypes,
|
||||
handleActions
|
||||
} from 'berkeleys-redux-utils';
|
||||
|
||||
import { types as app } from '../routes/Challenges/redux';
|
||||
|
||||
export const ns = 'entities';
|
||||
export const getNS = state => state[ns];
|
||||
export const entitiesSelector = getNS;
|
||||
export const types = createTypes([
|
||||
'updateUserPoints',
|
||||
'updateUserFlag',
|
||||
'updateUserEmail',
|
||||
'updateUserLang',
|
||||
'updateUserChallenge',
|
||||
'updateUserCurrentChallenge'
|
||||
], ns);
|
||||
|
||||
// updateUserPoints(username: String, points: Number) => Action
|
||||
export const updateUserPoints = createAction(
|
||||
types.updateUserPoints,
|
||||
(username, points) => ({ username, points })
|
||||
);
|
||||
// updateUserFlag(username: String, flag: String) => Action
|
||||
export const updateUserFlag = createAction(
|
||||
types.updateUserFlag,
|
||||
@ -34,21 +33,12 @@ export const updateUserLang = createAction(
|
||||
(username, lang) => ({ username, languageTag: lang })
|
||||
);
|
||||
|
||||
// updateUserChallenge(
|
||||
// username: String,
|
||||
// challengeInfo: Object
|
||||
// ) => Action
|
||||
export const updateUserChallenge = createAction(
|
||||
types.updateUserChallenge,
|
||||
(username, challengeInfo) => ({ username, challengeInfo })
|
||||
);
|
||||
|
||||
export const updateUserCurrentChallenge = createAction(
|
||||
types.updateUserCurrentChallenge
|
||||
);
|
||||
|
||||
|
||||
const initialState = {
|
||||
const defaultState = {
|
||||
superBlock: {},
|
||||
block: {},
|
||||
challenge: {},
|
||||
@ -69,14 +59,33 @@ export function makeSuperBlockSelector(name) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function createReducer() {
|
||||
const userReducer = handleActions(
|
||||
{
|
||||
[types.updateUserPoints]: (state, { payload: { username, points } }) => ({
|
||||
export const isChallengeLoaded = (state, { dashedName }) =>
|
||||
!!challengeMapSelector(state)[dashedName];
|
||||
|
||||
export default composeReducers(
|
||||
ns,
|
||||
function metaReducer(state = defaultState, action) {
|
||||
if (action.meta && action.meta.entities) {
|
||||
return {
|
||||
...state,
|
||||
...action.meta.entities
|
||||
};
|
||||
}
|
||||
return state;
|
||||
},
|
||||
handleActions(
|
||||
() => ({
|
||||
[
|
||||
app.submitChallenge.complete
|
||||
]: (state, { payload: { username, points, challengeInfo } }) => ({
|
||||
...state,
|
||||
[username]: {
|
||||
...state[username],
|
||||
points
|
||||
points,
|
||||
challengeMap: {
|
||||
...state[username].challengeMap,
|
||||
[challengeInfo.id]: challengeInfo
|
||||
}
|
||||
}
|
||||
}),
|
||||
[types.updateUserFlag]: (state, { payload: { username, flag } }) => ({
|
||||
@ -118,46 +127,8 @@ export default function createReducer() {
|
||||
...state[username],
|
||||
currentChallengeId
|
||||
}
|
||||
}),
|
||||
[types.updateUserChallenge]:
|
||||
(
|
||||
state,
|
||||
{
|
||||
payload: { username, challengeInfo }
|
||||
}
|
||||
) => ({
|
||||
...state,
|
||||
[username]: {
|
||||
...state[username],
|
||||
challengeMap: {
|
||||
...state[username].challengeMap,
|
||||
[challengeInfo.id]: challengeInfo
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
initialState.user
|
||||
);
|
||||
|
||||
function metaReducer(state = initialState, action) {
|
||||
if (action.meta && action.meta.entities) {
|
||||
return {
|
||||
...state,
|
||||
...action.meta.entities
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function entitiesReducer(state, action) {
|
||||
const newState = metaReducer(state, action);
|
||||
const user = userReducer(newState.user, action);
|
||||
if (newState.user !== user) {
|
||||
return { ...newState, user };
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
entitiesReducer.toString = () => ns;
|
||||
return entitiesReducer;
|
||||
}
|
||||
}),
|
||||
defaultState
|
||||
)
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { epics as app } from './redux';
|
||||
import { epics as challenge } from './routes/challenges/redux';
|
||||
import { epics as settings } from './routes/settings/redux';
|
||||
import { epics as challenge } from './routes/Challenges/redux';
|
||||
import { epics as settings } from './routes/Settings/redux';
|
||||
import { epics as nav } from './Nav/redux';
|
||||
import { epics as map } from './Map/redux';
|
||||
import { epics as panes } from './Panes/redux';
|
||||
|
97
common/app/files/index.js
Normal file
97
common/app/files/index.js
Normal file
@ -0,0 +1,97 @@
|
||||
import {
|
||||
combineActions,
|
||||
createAction,
|
||||
createTypes,
|
||||
handleActions
|
||||
} from 'berkeleys-redux-utils';
|
||||
|
||||
import { bonfire, html, js } from '../utils/challengeTypes.js';
|
||||
import { createPoly, setContent } from '../../utils/polyvinyl.js';
|
||||
import { arrayToString, buildSeed, getPreFile } from '../utils/classic-file.js';
|
||||
import { types as app } from '../redux';
|
||||
import { types as challenges } from '../routes/Challenges/redux';
|
||||
|
||||
const ns = 'files';
|
||||
|
||||
export const types = createTypes([
|
||||
'updateFile',
|
||||
'updateFiles',
|
||||
'savedCodeFound'
|
||||
], ns);
|
||||
|
||||
export const updateFile = createAction(types.updateFile);
|
||||
export const updateFiles = createAction(types.updateFiles);
|
||||
export const savedCodeFound = createAction(
|
||||
types.savedCodeFound,
|
||||
(files, challenge) => ({ files, challenge })
|
||||
);
|
||||
|
||||
export const filesSelector = state => state[ns];
|
||||
|
||||
export default handleActions(
|
||||
() => ({
|
||||
[types.updateFile]: (state, { payload: { key, content }}) => ({
|
||||
...state,
|
||||
[key]: setContent(content, state[key])
|
||||
}),
|
||||
[types.updateFiles]: (state, { payload: files }) => {
|
||||
return files
|
||||
.reduce((files, file) => {
|
||||
files[file.key] = file;
|
||||
return files;
|
||||
}, { ...state });
|
||||
},
|
||||
[types.savedCodeFound]: (state, { payload: { files, challenge } }) => {
|
||||
if (challenge.type === 'mod') {
|
||||
// this may need to change to update head/tail
|
||||
return challenge.files;
|
||||
}
|
||||
if (
|
||||
challenge.challengeType !== html &&
|
||||
challenge.challengeType !== js &&
|
||||
challenge.challengeType !== bonfire
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
// classic challenge to modern format
|
||||
const preFile = getPreFile(challenge);
|
||||
return {
|
||||
[preFile.key]: createPoly({
|
||||
...files[preFile.key],
|
||||
// make sure head/tail are always fresh
|
||||
head: arrayToString(challenge.head),
|
||||
tail: arrayToString(challenge.tail)
|
||||
})
|
||||
};
|
||||
},
|
||||
[
|
||||
combineActions(
|
||||
challenges.challengeUpdated,
|
||||
app.fetchChallenge.complete
|
||||
)
|
||||
]: (state, { payload: { challenge } }) => {
|
||||
if (challenge.type === 'mod') {
|
||||
return challenge.files;
|
||||
}
|
||||
if (
|
||||
challenge.challengeType !== html &&
|
||||
challenge.challengeType !== js &&
|
||||
challenge.challengeType !== bonfire
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
// classic challenge to modern format
|
||||
const preFile = getPreFile(challenge);
|
||||
return {
|
||||
[preFile.key]: createPoly({
|
||||
...preFile,
|
||||
contents: buildSeed(challenge),
|
||||
head: arrayToString(challenge.head),
|
||||
tail: arrayToString(challenge.tail)
|
||||
})
|
||||
};
|
||||
}
|
||||
}),
|
||||
{},
|
||||
ns
|
||||
);
|
@ -1 +1,3 @@
|
||||
export default from './create-app.jsx';
|
||||
export { default as createApp } from './create-app.jsx';
|
||||
export { default as App } from './App.jsx';
|
||||
export { default as provideStore } from './provide-store.js';
|
||||
|
@ -1,11 +1,11 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React from 'react';
|
||||
import { createElement } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
export default function provideStore(element, store) {
|
||||
return React.createElement(
|
||||
export default function provideStore(Component, store) {
|
||||
return createElement(
|
||||
Provider,
|
||||
{ store },
|
||||
element
|
||||
createElement(Component)
|
||||
);
|
||||
}
|
||||
|
26
common/app/reducer.js
Normal file
26
common/app/reducer.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { combineReducers } from 'berkeleys-redux-utils';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
|
||||
import app from './redux';
|
||||
import entities from './entities';
|
||||
import map from './Map/redux';
|
||||
import nav from './Nav/redux';
|
||||
import routes from './routes/redux';
|
||||
import toasts from './Toasts/redux';
|
||||
import files from './files';
|
||||
// not ideal but should go away once we move to react-redux-form
|
||||
import { projectNormalizer } from './routes/Challenges/redux';
|
||||
|
||||
const _formReducer = formReducer.normalize({ ...projectNormalizer });
|
||||
_formReducer.toString = () => 'form';
|
||||
|
||||
export default combineReducers(
|
||||
app,
|
||||
entities,
|
||||
map,
|
||||
nav,
|
||||
routes,
|
||||
toasts,
|
||||
files,
|
||||
_formReducer
|
||||
);
|
@ -9,21 +9,23 @@ import {
|
||||
delayedRedirect,
|
||||
|
||||
fetchChallengeCompleted,
|
||||
fetchChallengesCompleted,
|
||||
|
||||
langSelector
|
||||
fetchChallengesCompleted
|
||||
} from './';
|
||||
import { isChallengeLoaded } from '../entities/index.js';
|
||||
|
||||
import { shapeChallenges } from './utils';
|
||||
import { types as challenge } from '../routes/Challenges/redux';
|
||||
import { langSelector } from '../Router/redux';
|
||||
|
||||
const isDev = debug.enabled('fcc:*');
|
||||
|
||||
export function fetchChallengeEpic(actions, { getState }, { services }) {
|
||||
return actions::ofType('' + types.fetchChallenge)
|
||||
.flatMap(({ payload: { dashedName, block } }) => {
|
||||
const lang = langSelector(getState());
|
||||
return actions::ofType(challenge.onRouteChallenges)
|
||||
.filter(({ payload }) => !isChallengeLoaded(getState(), payload))
|
||||
.flatMapLatest(({ payload: params }) => {
|
||||
const options = {
|
||||
service: 'map',
|
||||
params: { block, dashedName, lang }
|
||||
params
|
||||
};
|
||||
return services.readService$(options)
|
||||
.retry(3)
|
||||
@ -52,11 +54,7 @@ export function fetchChallengesEpic(
|
||||
{ getState },
|
||||
{ services }
|
||||
) {
|
||||
return actions::ofType(
|
||||
// async type
|
||||
'' + types.fetchChallenges,
|
||||
types.appMounted
|
||||
)
|
||||
return actions::ofType(types.appMounted)
|
||||
.flatMapLatest(() => {
|
||||
const lang = langSelector(getState());
|
||||
const options = {
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
addThemeToBody
|
||||
} from './';
|
||||
|
||||
export default function getUserEpic(actions, { getState }, { services }) {
|
||||
export default function getUserEpic(actions, _, { services }) {
|
||||
return actions::ofType(types.fetchUser)
|
||||
.flatMap(() => {
|
||||
return services.readService$({ service: 'user' })
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { Observable } from 'rx';
|
||||
import { createTypes, createAsyncTypes } from 'redux-create-types';
|
||||
import { combineActions, createAction, handleActions } from 'redux-actions';
|
||||
import {
|
||||
combineActions,
|
||||
createAction,
|
||||
createAsyncTypes,
|
||||
createTypes,
|
||||
handleActions
|
||||
} from 'berkeleys-redux-utils';
|
||||
import { createSelector } from 'reselect';
|
||||
import noop from 'lodash/noop';
|
||||
import identity from 'lodash/identity';
|
||||
@ -10,6 +15,7 @@ import fetchUserEpic from './fetch-user-epic.js';
|
||||
import updateMyCurrentChallengeEpic from './update-my-challenge-epic.js';
|
||||
import fetchChallengesEpic from './fetch-challenges-epic.js';
|
||||
import navSizeEpic from './nav-size-epic.js';
|
||||
import { types as challenges } from '../routes/Challenges/redux';
|
||||
|
||||
import ns from '../ns.json';
|
||||
|
||||
@ -21,14 +27,14 @@ export const epics = [
|
||||
];
|
||||
|
||||
export const types = createTypes([
|
||||
'onRouteHome',
|
||||
|
||||
'appMounted',
|
||||
'analytics',
|
||||
'updateTitle',
|
||||
'updateAppLang',
|
||||
|
||||
createAsyncTypes('fetchChallenge'),
|
||||
createAsyncTypes('fetchChallenges'),
|
||||
'updateCurrentChallenge',
|
||||
|
||||
'fetchUser',
|
||||
'addUser',
|
||||
@ -80,6 +86,7 @@ export const createEventMetaCreator = ({
|
||||
}
|
||||
});
|
||||
|
||||
export const onRouteHome = createAction(types.onRouteHome);
|
||||
export const appMounted = createAction(types.appMounted);
|
||||
export const fetchChallenge = createAction(
|
||||
'' + types.fetchChallenge,
|
||||
@ -96,9 +103,6 @@ export const fetchChallengesCompleted = createAction(
|
||||
(entities, result) => ({ entities, result }),
|
||||
entities => ({ entities })
|
||||
);
|
||||
export const updateCurrentChallenge = createAction(
|
||||
types.updateCurrentChallenge
|
||||
);
|
||||
|
||||
// updateTitle(title: String) => Action
|
||||
export const updateTitle = createAction(types.updateTitle);
|
||||
@ -118,8 +122,6 @@ export const addUser = createAction(
|
||||
export const updateThisUser = createAction(types.updateThisUser);
|
||||
export const showSignIn = createAction(types.showSignIn);
|
||||
|
||||
export const updateAppLang = createAction(types.updateAppLang);
|
||||
|
||||
// used when server needs client to redirect
|
||||
export const delayedRedirect = createAction(types.delayedRedirect);
|
||||
|
||||
@ -156,7 +158,6 @@ const initialState = {
|
||||
title: 'Learn To Code | freeCodeCamp',
|
||||
isSignInAttempted: false,
|
||||
user: '',
|
||||
lang: '',
|
||||
csrfToken: '',
|
||||
theme: 'default',
|
||||
// eventually this should be only in the user object
|
||||
@ -165,7 +166,6 @@ const initialState = {
|
||||
};
|
||||
|
||||
export const getNS = state => state[ns];
|
||||
export const langSelector = state => getNS(state).lang;
|
||||
export const csrfSelector = state => getNS(state).csrfToken;
|
||||
export const themeSelector = state => getNS(state).theme;
|
||||
export const titleSelector = state => getNS(state).title;
|
||||
@ -180,6 +180,8 @@ export const userSelector = createSelector(
|
||||
(username, userMap) => userMap[username] || {}
|
||||
);
|
||||
|
||||
export const isSignedInSelector = state => !!userSelector(state).username;
|
||||
|
||||
export const challengeSelector = createSelector(
|
||||
currentChallengeSelector,
|
||||
state => entitiesSelector(state).challenge,
|
||||
@ -222,58 +224,46 @@ export const firstChallengeSelector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
export default function createReducer() {
|
||||
const reducer = handleActions(
|
||||
{
|
||||
[types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
|
||||
...state,
|
||||
title: payload + ' | freeCodeCamp'
|
||||
}),
|
||||
export default handleActions(
|
||||
() => ({
|
||||
[types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
|
||||
...state,
|
||||
title: payload + ' | freeCodeCamp'
|
||||
}),
|
||||
|
||||
[types.updateThisUser]: (state, { payload: user }) => ({
|
||||
...state,
|
||||
user
|
||||
}),
|
||||
[types.fetchChallenge.complete]: (state, { payload }) => ({
|
||||
...state,
|
||||
currentChallenge: payload.currentChallenge
|
||||
}),
|
||||
[combineActions(
|
||||
types.fetchChallenge.complete,
|
||||
types.fetchChallenges.complete
|
||||
)]: (state, { payload }) => ({
|
||||
...state,
|
||||
superBlocks: payload.result.superBlocks
|
||||
}),
|
||||
[types.updateCurrentChallenge]: (state, { payload = '' }) => ({
|
||||
...state,
|
||||
currentChallenge: payload
|
||||
}),
|
||||
[types.updateAppLang]: (state, { payload = 'en' }) =>({
|
||||
...state,
|
||||
lang: payload
|
||||
}),
|
||||
[types.updateTheme]: (state, { payload = 'default' }) => ({
|
||||
...state,
|
||||
theme: payload
|
||||
}),
|
||||
[combineActions(types.showSignIn, types.updateThisUser)]: state => ({
|
||||
...state,
|
||||
isSignInAttempted: true
|
||||
}),
|
||||
[types.updateThisUser]: (state, { payload: user }) => ({
|
||||
...state,
|
||||
user
|
||||
}),
|
||||
[combineActions(
|
||||
types.fetchChallenge.complete,
|
||||
types.fetchChallenges.complete
|
||||
)]: (state, { payload }) => ({
|
||||
...state,
|
||||
superBlocks: payload.result.superBlocks
|
||||
}),
|
||||
[challenges.onRouteChallenges]: (state, { payload: { dashedName } }) => ({
|
||||
...state,
|
||||
currentChallenge: dashedName
|
||||
}),
|
||||
[types.updateTheme]: (state, { payload = 'default' }) => ({
|
||||
...state,
|
||||
theme: payload
|
||||
}),
|
||||
[combineActions(types.showSignIn, types.updateThisUser)]: state => ({
|
||||
...state,
|
||||
isSignInAttempted: true
|
||||
}),
|
||||
|
||||
[types.challengeSaved]: (state, { payload: { points = 0 } }) => ({
|
||||
...state,
|
||||
points
|
||||
}),
|
||||
[types.delayedRedirect]: (state, { payload }) => ({
|
||||
...state,
|
||||
delayedRedirect: payload
|
||||
})
|
||||
},
|
||||
initialState
|
||||
);
|
||||
|
||||
reducer.toString = () => ns;
|
||||
return reducer;
|
||||
}
|
||||
[types.challengeSaved]: (state, { payload: { points = 0 } }) => ({
|
||||
...state,
|
||||
points
|
||||
}),
|
||||
[types.delayedRedirect]: (state, { payload }) => ({
|
||||
...state,
|
||||
delayedRedirect: payload
|
||||
})
|
||||
}),
|
||||
initialState,
|
||||
ns
|
||||
);
|
||||
|
@ -12,12 +12,15 @@ import {
|
||||
} from './';
|
||||
import { updateUserCurrentChallenge } from '../entities';
|
||||
import { postJSON$ } from '../../utils/ajax-stream';
|
||||
import { types as challenges } from '../routes/Challenges/redux';
|
||||
|
||||
const log = debug('fcc:app:redux:up-my-challenge-epic');
|
||||
export default function updateMyCurrentChallengeEpic(actions, { getState }) {
|
||||
const updateChallenge = actions::ofType(types.updateCurrentChallenge)
|
||||
const updateChallenge = actions::ofType(types.appMounted)
|
||||
.flatMapLatest(() => actions::ofType(challenges.onRouteChallenges))
|
||||
.map(() => {
|
||||
const state = getState();
|
||||
// username is never defined SSR
|
||||
const { username } = userSelector(state);
|
||||
const { id } = challengeSelector(state);
|
||||
const csrf = csrfSelector(state);
|
||||
|
@ -10,7 +10,7 @@ export function filterComingSoonBetaChallenge(
|
||||
}
|
||||
|
||||
export function filterComingSoonBetaFromEntities(
|
||||
{ challenge: challengeMap, block: blockMap, ...rest },
|
||||
{ challenge: challengeMap, block: blockMap = {}, ...rest },
|
||||
isDev = false
|
||||
) {
|
||||
const filter = filterComingSoonBetaChallenge.bind(null, isDev);
|
||||
|
19
common/app/routes-map.js
Normal file
19
common/app/routes-map.js
Normal file
@ -0,0 +1,19 @@
|
||||
import reduce from 'lodash/reduce';
|
||||
import { types } from './redux';
|
||||
import routes from './routes';
|
||||
|
||||
const base = '/:lang';
|
||||
|
||||
export default {
|
||||
...reduce(routes, (routes, route, type) => {
|
||||
let newRoute;
|
||||
if (typeof route === 'string') {
|
||||
newRoute = base + route;
|
||||
} else {
|
||||
newRoute = { ...route, path: base + route.path };
|
||||
}
|
||||
routes[type] = newRoute;
|
||||
return routes;
|
||||
}, {}),
|
||||
[types.routeOnHome]: base
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Modal } from 'react-bootstrap';
|
||||
import PureComponent from 'react-pure-render/component';
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Col, Row } from 'react-bootstrap';
|
||||
|
||||
import ns from './ns.json';
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ns from './ns.json';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { Grid, Col, Row } from 'react-bootstrap';
|
||||
|
@ -1,5 +1,6 @@
|
||||
import noop from 'lodash/noop';
|
||||
import React, { PureComponent, PropTypes } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Button, Modal } from 'react-bootstrap';
|
@ -1,4 +1,5 @@
|
||||
import React, { PureComponent, PropTypes } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import NoSSR from 'react-no-ssr';
|
||||
import Codemirror from 'react-codemirror';
|
||||
|
113
common/app/routes/Challenges/Show.jsx
Normal file
113
common/app/routes/Challenges/Show.jsx
Normal file
@ -0,0 +1,113 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { challengeMetaSelector } from './redux';
|
||||
|
||||
import CompletionModal from './Completion-Modal.jsx';
|
||||
import Classic from './views/classic';
|
||||
import Step from './views/step';
|
||||
import Project from './views/project';
|
||||
import BackEnd from './views/backend';
|
||||
import Quiz from './views/quiz';
|
||||
|
||||
import {
|
||||
fetchChallenge,
|
||||
|
||||
challengeSelector
|
||||
} from '../../redux';
|
||||
import { makeToast } from '../../Toasts/redux';
|
||||
import { paramsSelector } from '../../Router/redux';
|
||||
|
||||
const views = {
|
||||
backend: BackEnd,
|
||||
classic: Classic,
|
||||
project: Project,
|
||||
simple: Project,
|
||||
step: Step,
|
||||
quiz: Quiz
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchChallenge,
|
||||
makeToast
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
challengeSelector,
|
||||
challengeMetaSelector,
|
||||
paramsSelector,
|
||||
(
|
||||
{ dashedName, isTranslated },
|
||||
{ viewType },
|
||||
params,
|
||||
) => ({
|
||||
challenge: dashedName,
|
||||
isTranslated,
|
||||
params,
|
||||
viewType
|
||||
})
|
||||
);
|
||||
|
||||
const link = 'http://forum.freecodecamp.org/t/' +
|
||||
'guidelines-for-translating-free-code-camp' +
|
||||
'-to-any-language/19111';
|
||||
const helpUsTranslate = <a href={ link } target='_blank'>Help Us</a>;
|
||||
const propTypes = {
|
||||
isTranslated: PropTypes.bool,
|
||||
makeToast: PropTypes.func.isRequired,
|
||||
params: PropTypes.shape({
|
||||
block: PropTypes.string,
|
||||
dashedName: PropTypes.string,
|
||||
lang: PropTypes.string.isRequired
|
||||
}),
|
||||
viewType: PropTypes.string
|
||||
};
|
||||
|
||||
export class Show extends PureComponent {
|
||||
|
||||
isNotTranslated({ isTranslated, params: { lang } }) {
|
||||
return lang !== 'en' && !isTranslated;
|
||||
}
|
||||
|
||||
makeTranslateToast() {
|
||||
this.props.makeToast({
|
||||
message: 'We haven\'t translated this challenge yet.',
|
||||
action: helpUsTranslate,
|
||||
timeout: 15000
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.isNotTranslated(this.props)) {
|
||||
this.makeTranslateToast();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { params: { dashedName } } = nextProps;
|
||||
if (
|
||||
this.props.params.dashedName !== dashedName &&
|
||||
this.isNotTranslated(nextProps)
|
||||
) {
|
||||
this.makeTranslateToast();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { viewType } = this.props;
|
||||
const View = views[viewType] || Classic;
|
||||
return (
|
||||
<div>
|
||||
<View />
|
||||
<CompletionModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Show.displayName = 'Show(ChallengeView)';
|
||||
Show.propTypes = propTypes;
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Show);
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HelpBlock, FormGroup, FormControl } from 'react-bootstrap';
|
||||
import { getValidationState, DOMOnlyProps } from '../../utils/form';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes, PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Col, Row } from 'react-bootstrap';
|
||||
|
33
common/app/routes/Challenges/index.js
Normal file
33
common/app/routes/Challenges/index.js
Normal file
@ -0,0 +1,33 @@
|
||||
import { redirect } from 'redux-first-router';
|
||||
|
||||
import { types } from './redux';
|
||||
import { panesMap as backendPanesMap } from './views/backend';
|
||||
import { panesMap as classicPanesMap } from './views/classic';
|
||||
import { panesMap as stepPanesMap } from './views/step';
|
||||
import { panesMap as projectPanesMap } from './views/project';
|
||||
import { panesMap as quizPanesMap } from './views/quiz';
|
||||
|
||||
export const routes = {
|
||||
[types.onRouteChallengeRoot]: {
|
||||
path: '/challenges',
|
||||
thunk: (dispatch) =>
|
||||
dispatch(redirect({ type: types.onRouteCurrentChallenge }))
|
||||
},
|
||||
[types.onRouteChallenges]: '/challenges/:block/:dashedName',
|
||||
[types.onRouteCurrentChallenge]: '/challenges/current-challenge'
|
||||
};
|
||||
|
||||
export function createPanesMap() {
|
||||
return {
|
||||
// the route to use this panes map on
|
||||
[types.onRouteChallenges]: {
|
||||
[backendPanesMap]: backendPanesMap,
|
||||
[classicPanesMap]: classicPanesMap,
|
||||
[stepPanesMap]: stepPanesMap,
|
||||
[projectPanesMap]: projectPanesMap,
|
||||
[quizPanesMap]: quizPanesMap
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { default } from './Show.jsx';
|
@ -1,11 +1,10 @@
|
||||
import { ofType } from 'redux-epic';
|
||||
import {
|
||||
types,
|
||||
closeBugModal,
|
||||
|
||||
filesSelector
|
||||
closeBugModal
|
||||
} from '../redux';
|
||||
|
||||
import { filesSelector } from '../../../files';
|
||||
import { currentChallengeSelector } from '../../../redux';
|
||||
|
||||
function filesToMarkdown(files = {}) {
|
@ -1,13 +1,14 @@
|
||||
import debug from 'debug';
|
||||
import { Observable } from 'rx';
|
||||
import { combineEpics, ofType } from 'redux-epic';
|
||||
import { push } from 'react-router-redux';
|
||||
|
||||
import {
|
||||
types,
|
||||
|
||||
updateMain,
|
||||
challengeUpdated
|
||||
challengeUpdated,
|
||||
onRouteChallenges,
|
||||
onRouteCurrentChallenge,
|
||||
updateMain
|
||||
} from './';
|
||||
import { getNS as entitiesSelector } from '../../../entities';
|
||||
import {
|
||||
@ -16,40 +17,42 @@ import {
|
||||
getFirstChallengeOfNextSuperBlock
|
||||
} from '../utils';
|
||||
import {
|
||||
types as app,
|
||||
|
||||
createErrorObservable,
|
||||
updateCurrentChallenge,
|
||||
|
||||
currentChallengeSelector,
|
||||
challengeSelector,
|
||||
superBlocksSelector
|
||||
} from '../../../redux';
|
||||
import { langSelector } from '../../../Router/redux';
|
||||
import { makeToast } from '../../../Toasts/redux';
|
||||
|
||||
const isDev = debug.enabled('fcc:*');
|
||||
|
||||
// When we change challenge, update the current challenge
|
||||
// UI data.
|
||||
export function challengeUpdatedEpic(actions, { getState }) {
|
||||
return actions::ofType(app.updateCurrentChallenge)
|
||||
.flatMap(() => {
|
||||
const challenge = challengeSelector(getState());
|
||||
return Observable.of(
|
||||
challengeUpdated(challenge),
|
||||
push(`/challenges/${challenge.block}/${challenge.dashedName}`)
|
||||
);
|
||||
});
|
||||
return actions::ofType(types.onRouteChallenges)
|
||||
// prevent subsequent onRouteChallenges to cause UI to refresh
|
||||
.distinctUntilChanged(({ payload: { dashedName }}) => dashedName)
|
||||
.map(() => challengeSelector(getState()))
|
||||
// if the challenge isn't loaded in the current state,
|
||||
// this will be an empty object
|
||||
// We wait instead for the fetchChallenge.complete to complete the UI state
|
||||
.filter(({ dashedName }) => !!dashedName)
|
||||
.flatMap(challenge =>
|
||||
// send the challenge to update UI and update main iframe with inital
|
||||
// challenge
|
||||
Observable.of(challengeUpdated(challenge), updateMain())
|
||||
);
|
||||
}
|
||||
|
||||
// used to reset users code on request
|
||||
export function resetChallengeEpic(actions, { getState }) {
|
||||
return actions::ofType(types.resetChallenge)
|
||||
.flatMap(() => {
|
||||
const currentChallenge = currentChallengeSelector(getState());
|
||||
return Observable.of(
|
||||
updateCurrentChallenge(currentChallenge),
|
||||
return actions::ofType(types.clickOnReset)
|
||||
.flatMap(() =>
|
||||
Observable.of(
|
||||
challengeUpdated(challengeSelector(getState())),
|
||||
updateMain()
|
||||
);
|
||||
});
|
||||
));
|
||||
}
|
||||
|
||||
export function nextChallengeEpic(actions, { getState }) {
|
||||
@ -64,6 +67,7 @@ export function nextChallengeEpic(actions, { getState }) {
|
||||
const superBlocks = superBlocksSelector(state);
|
||||
const challenge = currentChallengeSelector(state);
|
||||
const entities = entitiesSelector(state);
|
||||
const lang = langSelector(state);
|
||||
nextChallenge = getNextChallenge(challenge, entities, { isDev });
|
||||
// block completed.
|
||||
if (!nextChallenge) {
|
||||
@ -107,11 +111,15 @@ export function nextChallengeEpic(actions, { getState }) {
|
||||
'that have not been passed yet. ',
|
||||
timeout: 15000
|
||||
}),
|
||||
push('/map')
|
||||
onRouteCurrentChallenge()
|
||||
);
|
||||
}
|
||||
return Observable.of(
|
||||
updateCurrentChallenge(nextChallenge.dashedName),
|
||||
// normally we wouldn't need to add the lang as
|
||||
// addLangToRoutesEnhancer should add langs for us, but the way
|
||||
// enhancers/middlewares and RFR orders things this action will not
|
||||
// see addLangToRoutesEnhancer and cause RFR to render NotFound
|
||||
onRouteChallenges({ lang, ...nextChallenge }),
|
||||
makeToast({ message: 'Your next challenge has arrived.' })
|
||||
);
|
||||
} catch (err) {
|
@ -2,48 +2,45 @@ import { Observable } from 'rx';
|
||||
import { ofType } from 'redux-epic';
|
||||
|
||||
import {
|
||||
types,
|
||||
|
||||
moveToNextChallenge,
|
||||
clearSavedCode,
|
||||
|
||||
challengeMetaSelector,
|
||||
filesSelector,
|
||||
testsSelector
|
||||
moveToNextChallenge,
|
||||
submitChallengeComplete,
|
||||
testsSelector,
|
||||
types
|
||||
} from './';
|
||||
|
||||
import {
|
||||
createErrorObservable,
|
||||
|
||||
challengeSelector,
|
||||
createErrorObservable,
|
||||
csrfSelector,
|
||||
userSelector
|
||||
} from '../../../redux';
|
||||
import {
|
||||
updateUserPoints,
|
||||
updateUserChallenge
|
||||
} from '../../../entities';
|
||||
import { filesSelector } from '../../../files';
|
||||
import { backEndProject } from '../../../utils/challengeTypes.js';
|
||||
import { makeToast } from '../../../Toasts/redux';
|
||||
import { postJSON$ } from '../../../../utils/ajax-stream.js';
|
||||
|
||||
function postChallenge(url, username, _csrf, challengeInfo) {
|
||||
const body = { ...challengeInfo, _csrf };
|
||||
const saveChallenge = postJSON$(url, body)
|
||||
.retry(3)
|
||||
.flatMap(({ points, lastUpdated, completedDate }) => {
|
||||
return Observable.of(
|
||||
updateUserPoints(username, points),
|
||||
updateUserChallenge(
|
||||
username,
|
||||
{ ...challengeInfo, lastUpdated, completedDate }
|
||||
),
|
||||
clearSavedCode()
|
||||
);
|
||||
})
|
||||
.catch(createErrorObservable);
|
||||
const challengeCompleted = Observable.of(moveToNextChallenge());
|
||||
return Observable.merge(saveChallenge, challengeCompleted);
|
||||
return Observable.if(
|
||||
() => !!username,
|
||||
Observable.defer(() => {
|
||||
const body = { ...challengeInfo, _csrf };
|
||||
const saveChallenge = postJSON$(url, body)
|
||||
.retry(3)
|
||||
.map(({ points, lastUpdated, completedDate }) =>
|
||||
submitChallengeComplete(
|
||||
username,
|
||||
points,
|
||||
{ ...challengeInfo, lastUpdated, completedDate }
|
||||
)
|
||||
)
|
||||
.catch(createErrorObservable);
|
||||
const challengeCompleted = Observable.of(moveToNextChallenge());
|
||||
return Observable.merge(saveChallenge, challengeCompleted)
|
||||
.startWith({ type: types.submitChallenge.start });
|
||||
}),
|
||||
Observable.of(moveToNextChallenge())
|
||||
);
|
||||
}
|
||||
|
||||
function submitModern(type, state) {
|
||||
@ -53,7 +50,7 @@ function submitModern(type, state) {
|
||||
return Observable.empty();
|
||||
}
|
||||
|
||||
if (type === types.submitChallenge) {
|
||||
if (type === types.submitChallenge.toString()) {
|
||||
const { id } = challengeSelector(state);
|
||||
const files = filesSelector(state);
|
||||
const { username } = userSelector(state);
|
||||
@ -145,7 +142,7 @@ const submitters = {
|
||||
};
|
||||
|
||||
export default function completionEpic(actions, { getState }) {
|
||||
return actions::ofType(types.checkChallenge, types.submitChallenge)
|
||||
return actions::ofType(types.checkChallenge, types.submitChallenge.toString())
|
||||
.flatMap(({ type, payload }) => {
|
||||
const state = getState();
|
||||
const { submitType } = challengeMetaSelector(state);
|
@ -2,11 +2,11 @@ import { ofType } from 'redux-epic';
|
||||
|
||||
import {
|
||||
types,
|
||||
updateFile,
|
||||
|
||||
keySelector
|
||||
} from './';
|
||||
|
||||
import { updateFile } from '../../../files';
|
||||
|
||||
export default function editorEpic(actions, { getState }) {
|
||||
return actions::ofType(types.classicEditorUpdated)
|
||||
.pluck('payload')
|
@ -1,5 +1,11 @@
|
||||
import { createTypes } from 'redux-create-types';
|
||||
import { createAction, combineActions, handleActions } from 'redux-actions';
|
||||
import {
|
||||
combineActions,
|
||||
combineReducers,
|
||||
createAction,
|
||||
createAsyncTypes,
|
||||
createTypes,
|
||||
handleActions
|
||||
} from 'berkeleys-redux-utils';
|
||||
import { createSelector } from 'reselect';
|
||||
import noop from 'lodash/noop';
|
||||
|
||||
@ -10,11 +16,7 @@ import editorEpic from './editor-epic.js';
|
||||
|
||||
import ns from '../ns.json';
|
||||
import {
|
||||
arrayToString,
|
||||
buildSeed,
|
||||
createTests,
|
||||
getFileKey,
|
||||
getPreFile,
|
||||
loggerToStr,
|
||||
submitTypes,
|
||||
viewTypes
|
||||
@ -23,12 +25,12 @@ import {
|
||||
types as app,
|
||||
challengeSelector
|
||||
} from '../../../redux';
|
||||
import { bonfire, html, js } from '../../../utils/challengeTypes';
|
||||
import blockNameify from '../../../utils/blockNameify';
|
||||
import { createPoly, setContent } from '../../../../utils/polyvinyl';
|
||||
import createStepReducer, { epics as stepEpics } from '../views/step/redux';
|
||||
import createQuizReducer from '../views/quiz/redux';
|
||||
import createProjectReducer from '../views/project/redux';
|
||||
import { html } from '../../../utils/challengeTypes.js';
|
||||
import blockNameify from '../../../utils/blockNameify.js';
|
||||
import { getFileKey } from '../../../utils/classic-file.js';
|
||||
import stepReducer, { epics as stepEpics } from '../views/step/redux';
|
||||
import quizReducer from '../views/quiz/redux';
|
||||
import projectReducer from '../views/project/redux';
|
||||
|
||||
// this is not great but is ok until we move to a different form type
|
||||
export projectNormalizer from '../views/project/redux';
|
||||
@ -42,21 +44,20 @@ export const epics = [
|
||||
];
|
||||
|
||||
export const types = createTypes([
|
||||
'onRouteChallengeRoot',
|
||||
'onRouteChallenges',
|
||||
'onRouteCurrentChallenge',
|
||||
// challenges
|
||||
// |- classic
|
||||
'classicEditorUpdated',
|
||||
'challengeUpdated',
|
||||
'resetChallenge',
|
||||
'clickOnReset',
|
||||
'updateHint',
|
||||
'lockUntrustedCode',
|
||||
'unlockUntrustedCode',
|
||||
'closeChallengeModal',
|
||||
'updateSuccessMessage',
|
||||
|
||||
// files
|
||||
'updateFile',
|
||||
'updateFiles',
|
||||
|
||||
// rechallenge
|
||||
'executeChallenge',
|
||||
'updateMain',
|
||||
@ -67,15 +68,9 @@ export const types = createTypes([
|
||||
'initOutput',
|
||||
'updateTests',
|
||||
'checkChallenge',
|
||||
'submitChallenge',
|
||||
createAsyncTypes('submitChallenge'),
|
||||
'moveToNextChallenge',
|
||||
|
||||
// code storage
|
||||
'saveCode',
|
||||
'loadCode',
|
||||
'savedCodeFound',
|
||||
'clearSavedCode',
|
||||
|
||||
// bug
|
||||
'openBugModal',
|
||||
'closeBugModal',
|
||||
@ -91,6 +86,11 @@ export const types = createTypes([
|
||||
'toggleStep'
|
||||
], ns);
|
||||
|
||||
// routes
|
||||
export const onRouteChallenges = createAction(types.onRouteChallenges);
|
||||
export const onRouteCurrentChallenge =
|
||||
createAction(types.onRouteCurrentChallenge);
|
||||
|
||||
// classic
|
||||
export const classicEditorUpdated = createAction(types.classicEditorUpdated);
|
||||
// challenges
|
||||
@ -106,10 +106,7 @@ export const challengeUpdated = createAction(
|
||||
types.challengeUpdated,
|
||||
challenge => ({ challenge })
|
||||
);
|
||||
export const resetChallenge = createAction(types.resetChallenge);
|
||||
// files
|
||||
export const updateFile = createAction(types.updateFile);
|
||||
export const updateFiles = createAction(types.updateFiles);
|
||||
export const clickOnReset = createAction(types.clickOnReset);
|
||||
|
||||
// rechallenge
|
||||
export const executeChallenge = createAction(
|
||||
@ -130,16 +127,12 @@ export const updateOutput = createAction(types.updateOutput, loggerToStr);
|
||||
export const checkChallenge = createAction(types.checkChallenge);
|
||||
|
||||
export const submitChallenge = createAction(types.submitChallenge);
|
||||
export const moveToNextChallenge = createAction(types.moveToNextChallenge);
|
||||
|
||||
// code storage
|
||||
export const saveCode = createAction(types.saveCode);
|
||||
export const loadCode = createAction(types.loadCode);
|
||||
export const savedCodeFound = createAction(
|
||||
types.savedCodeFound,
|
||||
(files, challenge) => ({ files, challenge })
|
||||
export const submitChallengeComplete = createAction(
|
||||
types.submitChallenge.complete,
|
||||
(username, points, challengeInfo) => ({ username, points, challengeInfo })
|
||||
);
|
||||
export const clearSavedCode = createAction(types.clearSavedCode);
|
||||
|
||||
export const moveToNextChallenge = createAction(types.moveToNextChallenge);
|
||||
|
||||
// bug
|
||||
export const openBugModal = createAction(types.openBugModal);
|
||||
@ -151,9 +144,7 @@ const initialUiState = {
|
||||
output: null,
|
||||
isChallengeModalOpen: false,
|
||||
isBugOpen: false,
|
||||
successMessage: 'Happy Coding!',
|
||||
hintIndex: 0,
|
||||
numOfHints: 0
|
||||
successMessage: 'Happy Coding!'
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
@ -163,7 +154,6 @@ const initialState = {
|
||||
helpChatRoom: 'Help',
|
||||
// old code storage key
|
||||
legacyKey: '',
|
||||
files: {},
|
||||
// map
|
||||
superBlocks: [],
|
||||
// misc
|
||||
@ -172,7 +162,6 @@ const initialState = {
|
||||
|
||||
export const getNS = state => state[ns];
|
||||
export const keySelector = state => getNS(state).key;
|
||||
export const filesSelector = state => getNS(state).files;
|
||||
export const testsSelector = state => getNS(state).tests;
|
||||
|
||||
export const outputSelector = state => getNS(state).output;
|
||||
@ -186,7 +175,8 @@ export const challengeModalSelector =
|
||||
export const bugModalSelector = state => getNS(state).isBugOpen;
|
||||
|
||||
export const challengeMetaSelector = createSelector(
|
||||
challengeSelector,
|
||||
// use closure to get around circular deps
|
||||
(...args) => challengeSelector(...args),
|
||||
challenge => {
|
||||
if (!challenge.id) {
|
||||
return {};
|
||||
@ -214,15 +204,15 @@ export const challengeMetaSelector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
export default function createReducers() {
|
||||
const setChallengeType = combineActions(
|
||||
types.challengeUpdated,
|
||||
app.fetchChallenge.complete
|
||||
);
|
||||
|
||||
const mainReducer = handleActions(
|
||||
{
|
||||
[setChallengeType]: (state, { payload: { challenge } }) => {
|
||||
export default combineReducers(
|
||||
handleActions(
|
||||
() => ({
|
||||
[
|
||||
combineActions(
|
||||
types.challengeUpdated,
|
||||
app.fetchChallenge.complete
|
||||
)
|
||||
]: (state, { payload: { challenge } }) => {
|
||||
return {
|
||||
...state,
|
||||
...initialUiState,
|
||||
@ -230,10 +220,7 @@ export default function createReducers() {
|
||||
challenge: challenge.dashedName,
|
||||
key: getFileKey(challenge),
|
||||
tests: createTests(challenge),
|
||||
helpChatRoom: challenge.helpRoom || 'Help',
|
||||
numOfHints: Array.isArray(challenge.hints) ?
|
||||
challenge.hints.length :
|
||||
0
|
||||
helpChatRoom: challenge.helpRoom || 'Help'
|
||||
};
|
||||
},
|
||||
[types.updateTests]: (state, { payload: tests }) => ({
|
||||
@ -252,12 +239,6 @@ export default function createReducers() {
|
||||
...state,
|
||||
successMessage: payload
|
||||
}),
|
||||
[types.updateHint]: state => ({
|
||||
...state,
|
||||
hintIndex: state.hintIndex + 1 >= state.numOfHints ?
|
||||
0 :
|
||||
state.hintIndex + 1
|
||||
}),
|
||||
[types.lockUntrustedCode]: state => ({
|
||||
...state,
|
||||
isCodeLocked: true
|
||||
@ -283,86 +264,11 @@ export default function createReducers() {
|
||||
|
||||
[types.openBugModal]: state => ({ ...state, isBugOpen: true }),
|
||||
[types.closeBugModal]: state => ({ ...state, isBugOpen: false })
|
||||
},
|
||||
initialState
|
||||
);
|
||||
|
||||
const filesReducer = handleActions(
|
||||
{
|
||||
[types.updateFile]: (state, { payload: { key, content }}) => ({
|
||||
...state,
|
||||
[key]: setContent(content, state[key])
|
||||
}),
|
||||
[types.updateFiles]: (state, { payload: files }) => {
|
||||
return files
|
||||
.reduce((files, file) => {
|
||||
files[file.key] = file;
|
||||
return files;
|
||||
}, { ...state });
|
||||
},
|
||||
[types.savedCodeFound]: (state, { payload: { files, challenge } }) => {
|
||||
if (challenge.type === 'mod') {
|
||||
// this may need to change to update head/tail
|
||||
return challenge.files;
|
||||
}
|
||||
if (
|
||||
challenge.challengeType !== html &&
|
||||
challenge.challengeType !== js &&
|
||||
challenge.challengeType !== bonfire
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
// classic challenge to modern format
|
||||
const preFile = getPreFile(challenge);
|
||||
return {
|
||||
[preFile.key]: createPoly({
|
||||
...files[preFile.key],
|
||||
// make sure head/tail are always fresh
|
||||
head: arrayToString(challenge.head),
|
||||
tail: arrayToString(challenge.tail)
|
||||
})
|
||||
};
|
||||
},
|
||||
[setChallengeType]: (state, { payload: { challenge } }) => {
|
||||
if (challenge.type === 'mod') {
|
||||
return challenge.files;
|
||||
}
|
||||
if (
|
||||
challenge.challengeType !== html &&
|
||||
challenge.challengeType !== js &&
|
||||
challenge.challengeType !== bonfire
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
// classic challenge to modern format
|
||||
const preFile = getPreFile(challenge);
|
||||
return {
|
||||
[preFile.key]: createPoly({
|
||||
...preFile,
|
||||
contents: buildSeed(challenge),
|
||||
head: arrayToString(challenge.head),
|
||||
tail: arrayToString(challenge.tail)
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
function reducer(state, action) {
|
||||
const newState = mainReducer(state, action);
|
||||
const files = filesReducer(state && state.files || {}, action);
|
||||
if (newState.files !== files) {
|
||||
return { ...newState, files };
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
reducer.toString = () => ns;
|
||||
return [
|
||||
reducer,
|
||||
...createStepReducer(),
|
||||
...createProjectReducer(),
|
||||
...createQuizReducer()
|
||||
];
|
||||
}
|
||||
}),
|
||||
initialState,
|
||||
ns
|
||||
),
|
||||
stepReducer,
|
||||
quizReducer,
|
||||
projectReducer
|
||||
);
|
@ -1,6 +1,4 @@
|
||||
import flow from 'lodash/flow';
|
||||
import * as challengeTypes from '../../utils/challengeTypes';
|
||||
import { decodeScriptTags } from '../../../utils/encode-decode';
|
||||
|
||||
// determine the component to view for each challenge
|
||||
export const viewTypes = {
|
||||
@ -43,36 +41,6 @@ export const submitTypes = {
|
||||
// has html that should be rendered
|
||||
export const descriptionRegex = /\<blockquote|\<ol|\<h4|\<table/;
|
||||
|
||||
export function arrayToString(seedData = ['']) {
|
||||
seedData = Array.isArray(seedData) ? seedData : [seedData];
|
||||
return seedData.reduce((seed, line) => '' + seed + line + '\n', '\n');
|
||||
}
|
||||
|
||||
export function buildSeed({ challengeSeed = [] } = {}) {
|
||||
return flow(
|
||||
arrayToString,
|
||||
decodeScriptTags
|
||||
)(challengeSeed);
|
||||
}
|
||||
|
||||
const pathsMap = {
|
||||
[ challengeTypes.html ]: 'html',
|
||||
[ challengeTypes.js ]: 'js',
|
||||
[ challengeTypes.bonfire ]: 'js'
|
||||
};
|
||||
|
||||
export function getPreFile({ challengeType }) {
|
||||
return {
|
||||
name: 'index',
|
||||
ext: pathsMap[challengeType] || 'html',
|
||||
key: getFileKey({ challengeType })
|
||||
};
|
||||
}
|
||||
|
||||
export function getFileKey({ challengeType }) {
|
||||
return 'index' + (pathsMap[challengeType] || 'html');
|
||||
}
|
||||
|
||||
export function createTests({ tests = [] }) {
|
||||
return tests
|
||||
.map(test => {
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes, PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createSelector } from 'reselect';
|
||||
import { reduxForm } from 'redux-form';
|
||||
import {
|
@ -3,15 +3,19 @@ import React from 'react';
|
||||
import BackEnd from './Back-End.jsx';
|
||||
import { types } from '../../redux';
|
||||
import Panes from '../../../../Panes';
|
||||
import { createPaneMap } from '../../../../Panes/redux';
|
||||
import _Map from '../../../../Map';
|
||||
import ChildContainer from '../../../../Child-Container.jsx';
|
||||
|
||||
const propTypes = {};
|
||||
|
||||
export const panesMap = {
|
||||
[types.toggleMap]: 'Map',
|
||||
[types.toggleMain]: 'Main'
|
||||
};
|
||||
export const panesMap = createPaneMap(
|
||||
'backend',
|
||||
() => ({
|
||||
[types.toggleMap]: 'Map',
|
||||
[types.toggleMain]: 'Main'
|
||||
})
|
||||
);
|
||||
|
||||
const nameToComponentDef = {
|
||||
Map: {
|
@ -1,4 +1,5 @@
|
||||
import React, { PureComponent, PropTypes } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
@ -12,10 +13,11 @@ import {
|
||||
executeChallenge,
|
||||
classicEditorUpdated,
|
||||
challengeMetaSelector,
|
||||
filesSelector,
|
||||
keySelector
|
||||
} from '../../redux';
|
||||
|
||||
import { filesSelector } from '../../../../files';
|
||||
|
||||
const envProps = typeof window !== 'undefined' ? Object.keys(window) : [];
|
||||
const options = {
|
||||
lint: {
|
@ -3,19 +3,26 @@ import React from 'react';
|
||||
import SidePanel from './Side-Panel.jsx';
|
||||
import Editor from './Editor.jsx';
|
||||
import Preview from './Preview.jsx';
|
||||
import { types } from '../../redux';
|
||||
import { types, challengeMetaSelector } from '../../redux';
|
||||
import Panes from '../../../../Panes';
|
||||
import { createPaneMap } from '../../../../Panes/redux';
|
||||
import _Map from '../../../../Map';
|
||||
import ChildContainer from '../../../../Child-Container.jsx';
|
||||
|
||||
const propTypes = {};
|
||||
|
||||
export const panesMap = {
|
||||
[types.toggleMap]: 'Map',
|
||||
[types.toggleSidePanel]: 'Side Panel',
|
||||
[types.toggleClassicEditor]: 'Editor',
|
||||
[types.togglePreview]: 'Preview'
|
||||
};
|
||||
export const panesMap = createPaneMap(
|
||||
'classic',
|
||||
() => ({
|
||||
[types.toggleMap]: 'Map',
|
||||
[types.toggleSidePanel]: 'Side Panel',
|
||||
[types.toggleClassicEditor]: 'Editor',
|
||||
[types.togglePreview]: {
|
||||
name: 'Preview',
|
||||
filter: state => !!challengeMetaSelector(state).showPreview
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const nameToComponent = {
|
||||
Map: {
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDom from 'react-dom';
|
||||
import { createSelector } from 'reselect';
|
||||
import { connect } from 'react-redux';
|
||||
@ -46,10 +47,7 @@ const mapStateToProps = createSelector(
|
||||
codeLockedSelector,
|
||||
chatRoomSelector,
|
||||
(
|
||||
{
|
||||
description,
|
||||
hints = []
|
||||
},
|
||||
{ description },
|
||||
{ title },
|
||||
tests,
|
||||
output,
|
||||
@ -61,7 +59,6 @@ const mapStateToProps = createSelector(
|
||||
description,
|
||||
tests,
|
||||
output,
|
||||
hint: hints[hintIndex],
|
||||
isCodeLocked,
|
||||
helpChatRoom
|
||||
})
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, ButtonGroup, Tooltip, OverlayTrigger } from 'react-bootstrap';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
|
||||
@ -39,7 +40,7 @@ export default class ToolPanel extends PureComponent {
|
||||
this.props.makeToast({
|
||||
message: 'This will restore your code editor to its original state.',
|
||||
action: 'clear my code',
|
||||
actionCreator: 'resetChallenge',
|
||||
actionCreator: 'clickOnReset',
|
||||
timeout: 4000
|
||||
});
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { reduxForm } from 'redux-form';
|
||||
import {
|
||||
Button,
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createSelector } from 'reselect';
|
||||
import { connect } from 'react-redux';
|
||||
import PureComponent from 'react-pure-render/component';
|
@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import ns from './ns.json';
|
||||
import Main from './Project.jsx';
|
||||
import { types } from '../../redux';
|
||||
import Panes from '../../../../Panes';
|
||||
import { createPaneMap } from '../../../../Panes/redux';
|
||||
import _Map from '../../../../Map';
|
||||
import ChildContainer from '../../../../Child-Container.jsx';
|
||||
|
||||
const propTypes = {};
|
||||
export const panesMap = {
|
||||
[types.toggleMap]: 'Map',
|
||||
[types.toggleMain]: 'Main'
|
||||
};
|
||||
export const panesMap = createPaneMap(
|
||||
ns,
|
||||
() => ({
|
||||
[types.toggleMap]: 'Map',
|
||||
[types.toggleMain]: 'Main'
|
||||
})
|
||||
);
|
||||
|
||||
const nameToComponent = {
|
||||
Map: {
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes, PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ChallengeTitle from '../../Challenge-Title.jsx';
|
||||
|
||||
const propTypes = {
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
@ -1,5 +1,8 @@
|
||||
import { createTypes } from 'redux-create-types';
|
||||
import { createAction, handleActions } from 'redux-actions';
|
||||
import {
|
||||
createAction,
|
||||
createTypes,
|
||||
handleActions
|
||||
} from 'berkeleys-redux-utils';
|
||||
import ns from '../ns.json';
|
||||
|
||||
export const types = createTypes([
|
||||
@ -14,14 +17,13 @@ const initialState = {
|
||||
};
|
||||
export const submittingSelector = state => state[ns].isSubmitting;
|
||||
|
||||
export default function createReducer() {
|
||||
const reducer = handleActions({
|
||||
export default handleActions(
|
||||
() => ({
|
||||
[types.showProjectSubmit]: state => ({
|
||||
...state,
|
||||
isSubmitting: true
|
||||
})
|
||||
}, initialState);
|
||||
|
||||
reducer.toString = () => ns;
|
||||
return [ reducer ];
|
||||
}
|
||||
}),
|
||||
initialState,
|
||||
ns
|
||||
);
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes, PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import classnames from 'classnames';
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes, PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import ns from './ns.json';
|
||||
import Main from './Quiz.jsx';
|
||||
import { types } from '../../redux';
|
||||
import Panes from '../../../../Panes';
|
||||
import { createPaneMap } from '../../../../Panes/redux';
|
||||
import _Map from '../../../../Map';
|
||||
import ChildContainer from '../../../../Child-Container.jsx';
|
||||
|
||||
const propTypes = {};
|
||||
export const panesMap = {
|
||||
[types.toggleMap]: 'Map',
|
||||
[types.toggleMain]: 'Main'
|
||||
};
|
||||
export const panesMap = createPaneMap(
|
||||
ns,
|
||||
() => ({
|
||||
[types.toggleMap]: 'Map',
|
||||
[types.toggleMain]: 'Main'
|
||||
})
|
||||
);
|
||||
|
||||
const nameToComponent = {
|
||||
Map: {
|
@ -1,5 +1,8 @@
|
||||
import { createTypes } from 'redux-create-types';
|
||||
import { createAction, handleActions } from 'redux-actions';
|
||||
import {
|
||||
createAction,
|
||||
createTypes,
|
||||
handleActions
|
||||
} from 'berkeleys-redux-utils';
|
||||
import noop from 'lodash/noop';
|
||||
|
||||
import ns from '../ns.json';
|
||||
@ -48,8 +51,8 @@ export const currentIndexSelector = state => getNS(state).currentIndex;
|
||||
export const selectedChoiceSelector = state => getNS(state).selectedChoice;
|
||||
export const correctSelector = state => getNS(state).correct;
|
||||
|
||||
export default function createReducers() {
|
||||
const reducer = handleActions({
|
||||
export default handleActions(
|
||||
() => ({
|
||||
[types.nextQuestion]: state => ({
|
||||
...state,
|
||||
currentIndex: state.currentIndex + 1
|
||||
@ -72,8 +75,7 @@ export default function createReducers() {
|
||||
...state,
|
||||
selectedChoice: null
|
||||
})
|
||||
}, initialState);
|
||||
|
||||
reducer.toString = () => ns;
|
||||
return [ reducer ];
|
||||
}
|
||||
}),
|
||||
initialState,
|
||||
ns
|
||||
);
|
@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import ns from './ns.json';
|
||||
import Step from './Step.jsx';
|
||||
import { types } from '../../redux';
|
||||
import Panes from '../../../../Panes';
|
||||
import { createPaneMap } from '../../../../Panes/redux';
|
||||
import _Map from '../../../../Map';
|
||||
import ChildContainer from '../../../../Child-Container.jsx';
|
||||
|
||||
const propTypes = {};
|
||||
export const panesMap = {
|
||||
[types.toggleMap]: 'Map',
|
||||
[types.toggleStep]: 'Step'
|
||||
};
|
||||
export const panesMap = createPaneMap(
|
||||
ns,
|
||||
() => ({
|
||||
[types.toggleMap]: 'Map',
|
||||
[types.toggleStep]: 'Step'
|
||||
})
|
||||
);
|
||||
|
||||
const nameToComponent = {
|
||||
Map: {
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user