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"],
|
"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": {
|
"extends": "freecodecamp"
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ function formatFields({ type, ...fields }) {
|
|||||||
}, { type });
|
}, { type });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function analyticsSaga(actions, { getState }, { window }) {
|
export default function analyticsSaga(actions, _, { window }) {
|
||||||
const { ga } = window;
|
const { ga } = window;
|
||||||
if (typeof ga !== 'function') {
|
if (typeof ga !== 'function') {
|
||||||
console.log('GA not found');
|
console.log('GA not found');
|
||||||
|
@ -7,20 +7,21 @@ import { removeCodeUri, getCodeUri } from '../utils/code-uri';
|
|||||||
import { setContent } from '../../common/utils/polyvinyl';
|
import { setContent } from '../../common/utils/polyvinyl';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
types as app,
|
||||||
userSelector,
|
userSelector,
|
||||||
challengeSelector
|
challengeSelector
|
||||||
} from '../../common/app/redux';
|
} from '../../common/app/redux';
|
||||||
import { makeToast } from '../../common/app/Toasts/redux';
|
import { makeToast } from '../../common/app/Toasts/redux';
|
||||||
import {
|
import {
|
||||||
types,
|
types,
|
||||||
savedCodeFound,
|
|
||||||
updateMain,
|
updateMain,
|
||||||
lockUntrustedCode,
|
lockUntrustedCode,
|
||||||
|
|
||||||
keySelector,
|
keySelector,
|
||||||
filesSelector,
|
|
||||||
codeLockedSelector
|
codeLockedSelector
|
||||||
} from '../../common/app/routes/challenges/redux';
|
} from '../../common/app/routes/Challenges/redux';
|
||||||
|
|
||||||
|
import { filesSelector, savedCodeFound } from '../../common/app/files';
|
||||||
|
|
||||||
const legacyPrefixes = [
|
const legacyPrefixes = [
|
||||||
'Bonfire: ',
|
'Bonfire: ',
|
||||||
@ -59,18 +60,19 @@ function legacyToFile(code, files, key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearCodeEpic(actions, { getState }) {
|
export function clearCodeEpic(actions, { getState }) {
|
||||||
return actions::ofType(types.clearSavedCode)
|
return actions::ofType(types.submitChallenge.complete)
|
||||||
.map(() => {
|
.do(() => {
|
||||||
const { id } = challengeSelector(getState());
|
const { id } = challengeSelector(getState());
|
||||||
store.remove(id);
|
store.remove(id);
|
||||||
})
|
})
|
||||||
.ignoreElements();
|
.ignoreElements();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveCodeEpic(actions, { getState }) {
|
export function saveCodeEpic(actions, { getState }) {
|
||||||
return actions::ofType(types.saveCode)
|
return actions::ofType(types.executeChallenge)
|
||||||
// do not save challenge if code is locked
|
// do not save challenge if code is locked
|
||||||
.filter(() => !codeLockedSelector(getState()))
|
.filter(() => !codeLockedSelector(getState()))
|
||||||
.map(() => {
|
.do(() => {
|
||||||
const { id } = challengeSelector(getState());
|
const { id } = challengeSelector(getState());
|
||||||
const files = filesSelector(getState());
|
const files = filesSelector(getState());
|
||||||
store.set(id, files);
|
store.set(id, files);
|
||||||
@ -79,7 +81,11 @@ export function saveCodeEpic(actions, { getState }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function loadCodeEpic(actions, { getState }, { window, location }) {
|
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(() => {
|
.flatMap(() => {
|
||||||
let finalFiles;
|
let finalFiles;
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -17,11 +17,11 @@ import {
|
|||||||
frameMain,
|
frameMain,
|
||||||
frameTests,
|
frameTests,
|
||||||
initOutput,
|
initOutput,
|
||||||
saveCode,
|
|
||||||
|
|
||||||
filesSelector,
|
|
||||||
codeLockedSelector
|
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 }) {
|
export default function executeChallengeEpic(actions, { getState }) {
|
||||||
return actions::ofType(types.executeChallenge, types.updateMain)
|
return actions::ofType(types.executeChallenge, types.updateMain)
|
||||||
@ -47,7 +47,7 @@ export default function executeChallengeEpic(actions, { getState }) {
|
|||||||
frameMain(payload)
|
frameMain(payload)
|
||||||
];
|
];
|
||||||
if (type === types.executeChallenge) {
|
if (type === types.executeChallenge) {
|
||||||
actions.push(saveCode(), frameTests(payload));
|
actions.push(frameTests(payload));
|
||||||
}
|
}
|
||||||
return Observable.from(actions, null, null, Scheduler.default);
|
return Observable.from(actions, null, null, Scheduler.default);
|
||||||
})
|
})
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
|
|
||||||
codeLockedSelector,
|
codeLockedSelector,
|
||||||
testsSelector
|
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
|
// 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
|
// main iframe is responsible rendering the preview and is where we proxy the
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { types } from '../../common/app/redux';
|
import { types } from '../../common/app/redux';
|
||||||
import { ofType } from 'redux-epic';
|
import { ofType } from 'redux-epic';
|
||||||
|
|
||||||
export default function hardGoToSaga(actions, { getState }, { history }) {
|
export default function hardGoToSaga(actions, _, { history }) {
|
||||||
return actions::ofType(types.hardGoTo)
|
return actions::ofType(types.hardGoTo)
|
||||||
.map(({ payload = '/settings' }) => {
|
.map(({ payload = '/settings' }) => {
|
||||||
history.pushState(history.state, null, payload);
|
history.pushState(history.state, null, payload);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import MouseTrap from 'mousetrap';
|
import MouseTrap from 'mousetrap';
|
||||||
import { push } from 'react-router-redux';
|
import { push } from 'redux-first-router';
|
||||||
import {
|
import {
|
||||||
toggleNightMode,
|
toggleNightMode,
|
||||||
hardGoTo
|
hardGoTo
|
||||||
|
@ -1,21 +1,13 @@
|
|||||||
import './es6-shims';
|
import './es6-shims';
|
||||||
import Rx from 'rx';
|
import Rx from 'rx';
|
||||||
import React from 'react';
|
|
||||||
import debug from 'debug';
|
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 { render } from 'redux-epic';
|
||||||
import { createHistory } from 'history';
|
import createHistory from 'history/createBrowserHistory';
|
||||||
import useLangRoutes from './utils/use-lang-routes';
|
import useLangRoutes from './utils/use-lang-routes';
|
||||||
import sendPageAnalytics from './utils/send-page-analytics';
|
import sendPageAnalytics from './utils/send-page-analytics';
|
||||||
import flashToToast from './utils/flash-to-toast';
|
import flashToToast from './utils/flash-to-toast';
|
||||||
|
|
||||||
import createApp from '../common/app';
|
import { App, createApp, provideStore } from '../common/app';
|
||||||
import provideStore from '../common/app/provide-store';
|
|
||||||
import { getLangFromPath } from '../common/app/utils/lang';
|
import { getLangFromPath } from '../common/app/utils/lang';
|
||||||
|
|
||||||
// client specific epics
|
// client specific epics
|
||||||
@ -32,13 +24,13 @@ const log = debug('fcc:client');
|
|||||||
const hotReloadTimeout = 2000;
|
const hotReloadTimeout = 2000;
|
||||||
const { csrf: { token: csrfToken } = {} } = window.__fcc__;
|
const { csrf: { token: csrfToken } = {} } = window.__fcc__;
|
||||||
const DOMContainer = document.getElementById('fcc');
|
const DOMContainer = document.getElementById('fcc');
|
||||||
const initialState = isColdStored() ?
|
const defaultState = isColdStored() ?
|
||||||
getColdStorage() :
|
getColdStorage() :
|
||||||
window.__fcc__.data;
|
window.__fcc__.data;
|
||||||
const primaryLang = getLangFromPath(window.location.pathname);
|
const primaryLang = getLangFromPath(window.location.pathname);
|
||||||
|
|
||||||
initialState.app.csrfToken = csrfToken;
|
defaultState.app.csrfToken = csrfToken;
|
||||||
initialState.toasts = flashToToast(window.__fcc__.flash);
|
defaultState.toasts = flashToToast(window.__fcc__.flash);
|
||||||
|
|
||||||
// make empty object so hot reload works
|
// make empty object so hot reload works
|
||||||
window.__fcc__ = {};
|
window.__fcc__ = {};
|
||||||
@ -49,7 +41,6 @@ const history = useLangRoutes(createHistory, primaryLang)();
|
|||||||
sendPageAnalytics(history, window.ga);
|
sendPageAnalytics(history, window.ga);
|
||||||
|
|
||||||
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
|
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
|
||||||
const adjustUrlOnReplay = !!window.devToolsExtension;
|
|
||||||
|
|
||||||
const epicOptions = {
|
const epicOptions = {
|
||||||
isDev,
|
isDev,
|
||||||
@ -61,14 +52,10 @@ const epicOptions = {
|
|||||||
|
|
||||||
createApp({
|
createApp({
|
||||||
history,
|
history,
|
||||||
syncHistoryWithStore,
|
|
||||||
syncOptions: { adjustUrlOnReplay },
|
|
||||||
serviceOptions,
|
serviceOptions,
|
||||||
initialState,
|
defaultState,
|
||||||
middlewares: [ routerMiddleware(history) ],
|
|
||||||
epics,
|
epics,
|
||||||
epicOptions,
|
epicOptions,
|
||||||
reducers: { routing },
|
|
||||||
enhancers: [ devTools ]
|
enhancers: [ devTools ]
|
||||||
})
|
})
|
||||||
.doOnNext(({ store }) => {
|
.doOnNext(({ store }) => {
|
||||||
@ -83,13 +70,7 @@ createApp({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.doOnNext(() => log('rendering'))
|
.doOnNext(() => log('rendering'))
|
||||||
.flatMap(
|
.flatMap(({ store }) => render(provideStore(App, store), DOMContainer))
|
||||||
({ props, store }) => render(
|
|
||||||
provideStore(React.createElement(Router, props), store),
|
|
||||||
DOMContainer
|
|
||||||
),
|
|
||||||
({ store }) => store
|
|
||||||
)
|
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => debug('react rendered'),
|
() => debug('react rendered'),
|
||||||
err => { throw err; },
|
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 { connect } from 'react-redux';
|
||||||
|
|
||||||
import ns from './ns.json';
|
import ns from './ns.json';
|
||||||
import {
|
import {
|
||||||
appMounted,
|
appMounted,
|
||||||
fetchUser,
|
fetchUser,
|
||||||
updateAppLang,
|
|
||||||
|
|
||||||
userSelector
|
isSignedInSelector
|
||||||
} from './redux';
|
} from './redux';
|
||||||
|
|
||||||
import Nav from './Nav';
|
import Nav from './Nav';
|
||||||
import Toasts from './Toasts';
|
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 = {
|
const mapDispatchToProps = {
|
||||||
appMounted,
|
appMounted,
|
||||||
fetchUser,
|
fetchUser
|
||||||
updateAppLang
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const { username } = userSelector(state);
|
const isSignedIn = isSignedInSelector(state);
|
||||||
|
const route = mainRouteSelector(state);
|
||||||
return {
|
return {
|
||||||
toast: state.app.toast,
|
toast: state.app.toast,
|
||||||
isSignedIn: !!username
|
isSignedIn,
|
||||||
|
route
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -32,19 +37,17 @@ const propTypes = {
|
|||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
fetchUser: PropTypes.func,
|
fetchUser: PropTypes.func,
|
||||||
isSignedIn: PropTypes.bool,
|
isSignedIn: PropTypes.bool,
|
||||||
params: PropTypes.object,
|
route: PropTypes.string,
|
||||||
toast: PropTypes.object,
|
toast: PropTypes.object
|
||||||
updateAppLang: PropTypes.func.isRequired
|
};
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
challenges: Challenges,
|
||||||
|
settings: Settings
|
||||||
};
|
};
|
||||||
|
|
||||||
// export plain class for testing
|
// export plain class for testing
|
||||||
export class FreeCodeCamp extends React.Component {
|
export class FreeCodeCamp extends React.Component {
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (this.props.params.lang !== nextProps.params.lang) {
|
|
||||||
this.props.updateAppLang(nextProps.params.lang);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.appMounted();
|
this.props.appMounted();
|
||||||
if (!this.props.isSignedIn) {
|
if (!this.props.isSignedIn) {
|
||||||
@ -53,14 +56,18 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
route
|
||||||
|
} = this.props;
|
||||||
|
const Child = routes[route] || NotFound;
|
||||||
// we render nav after the content
|
// we render nav after the content
|
||||||
// to allow the panes to update
|
// to allow the panes to update
|
||||||
// redux store, which will update the bin
|
// redux store, which will update the bin
|
||||||
// buttons in the nav
|
// buttons in the nav
|
||||||
return (
|
return (
|
||||||
<div className={ `${ns}-container` }>
|
<div className={ `${ns}-container` }>
|
||||||
{ this.props.children }
|
|
||||||
<Nav />
|
<Nav />
|
||||||
|
<Child />
|
||||||
<Toasts />
|
<Toasts />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import ns from './ns.json';
|
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 { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import FA from 'react-fontawesome';
|
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 { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { Link } from 'react-router';
|
|
||||||
import PureComponent from 'react-pure-render/component';
|
import PureComponent from 'react-pure-render/component';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
@ -13,6 +13,8 @@ import {
|
|||||||
} from './redux';
|
} from './redux';
|
||||||
import { userSelector } from '../redux';
|
import { userSelector } from '../redux';
|
||||||
import { challengeMapSelector } from '../entities';
|
import { challengeMapSelector } from '../entities';
|
||||||
|
import { Link } from '../Router';
|
||||||
|
import { onRouteChallenges } from '../routes/Challenges/redux';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
block: PropTypes.string,
|
block: PropTypes.string,
|
||||||
@ -27,15 +29,7 @@ const propTypes = {
|
|||||||
isRequired: PropTypes.bool,
|
isRequired: PropTypes.bool,
|
||||||
title: PropTypes.string
|
title: PropTypes.string
|
||||||
};
|
};
|
||||||
function mapDispatchToProps(dispatch, { dashedName }) {
|
const mapDispatchToProps = { clickOnChallenge };
|
||||||
const dispatchers = {
|
|
||||||
clickOnChallenge: e => {
|
|
||||||
e.preventDefault();
|
|
||||||
return dispatch(clickOnChallenge(dashedName));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return () => dispatchers;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeMapStateToProps(_, { dashedName }) {
|
function makeMapStateToProps(_, { dashedName }) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
@ -152,8 +146,11 @@ export class Challenge extends PureComponent {
|
|||||||
className={ challengeClassName }
|
className={ challengeClassName }
|
||||||
key={ title }
|
key={ title }
|
||||||
>
|
>
|
||||||
<Link to={ `/challenges/${block}/${dashedName}` }>
|
<Link
|
||||||
<span onClick={ clickOnChallenge }>
|
onClick={ clickOnChallenge }
|
||||||
|
to={ onRouteChallenges({ dashedName, block }) }
|
||||||
|
>
|
||||||
|
<span >
|
||||||
{ title }
|
{ title }
|
||||||
{ this.renderCompleted(isCompleted, isLocked) }
|
{ this.renderCompleted(isCompleted, isLocked) }
|
||||||
{ this.renderRequired(isRequired) }
|
{ 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 { connect } from 'react-redux';
|
||||||
import PureComponent from 'react-pure-render/component';
|
import PureComponent from 'react-pure-render/component';
|
||||||
import { Col, Row } from 'react-bootstrap';
|
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 { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import PureComponent from 'react-pure-render/component';
|
import PureComponent from 'react-pure-render/component';
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { createTypes } from 'redux-create-types';
|
import {
|
||||||
import { createAction, handleActions } from 'redux-actions';
|
createAction,
|
||||||
|
createTypes,
|
||||||
|
handleActions
|
||||||
|
} from 'berkeleys-redux-utils';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import identity from 'lodash/identity';
|
import noop from 'lodash/noop';
|
||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
|
|
||||||
import selectChallengeEpic from './select-challenge-epic.js';
|
|
||||||
|
|
||||||
import * as utils from './utils.js';
|
import * as utils from './utils.js';
|
||||||
import ns from '../ns.json';
|
import ns from '../ns.json';
|
||||||
import {
|
import {
|
||||||
@ -13,11 +14,10 @@ import {
|
|||||||
createEventMetaCreator
|
createEventMetaCreator
|
||||||
} from '../../redux';
|
} from '../../redux';
|
||||||
|
|
||||||
export const epics = [
|
export const epics = [];
|
||||||
selectChallengeEpic
|
|
||||||
];
|
|
||||||
|
|
||||||
export const types = createTypes([
|
export const types = createTypes([
|
||||||
|
'onRouteMap',
|
||||||
'initMap',
|
'initMap',
|
||||||
|
|
||||||
'toggleThisPanel',
|
'toggleThisPanel',
|
||||||
@ -37,7 +37,7 @@ export const collapseAll = createAction(types.collapseAll);
|
|||||||
export const expandAll = createAction(types.expandAll);
|
export const expandAll = createAction(types.expandAll);
|
||||||
export const clickOnChallenge = createAction(
|
export const clickOnChallenge = createAction(
|
||||||
types.clickOnChallenge,
|
types.clickOnChallenge,
|
||||||
identity,
|
noop,
|
||||||
createEventMetaCreator({
|
createEventMetaCreator({
|
||||||
category: capitalize(ns),
|
category: capitalize(ns),
|
||||||
action: 'click',
|
action: 'click',
|
||||||
@ -88,9 +88,8 @@ export function makePanelHiddenSelector(name) {
|
|||||||
// }]
|
// }]
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
export default function createReducer() {
|
export default handleActions(
|
||||||
const reducer = handleActions(
|
()=> ({
|
||||||
{
|
|
||||||
[types.toggleThisPanel]: (state, { payload: name }) => {
|
[types.toggleThisPanel]: (state, { payload: name }) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -120,10 +119,7 @@ export default function createReducer() {
|
|||||||
mapUi: utils.createMapUi(entities, result)
|
mapUi: utils.createMapUi(entities, result)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
initialState
|
initialState,
|
||||||
|
ns
|
||||||
);
|
);
|
||||||
|
|
||||||
reducer.toString = () => ns;
|
|
||||||
return reducer;
|
|
||||||
}
|
|
||||||
|
@ -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';
|
import { NavItem } from 'react-bootstrap';
|
||||||
|
|
||||||
const propTypes = {
|
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 { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { LinkContainer } from 'react-router-bootstrap';
|
|
||||||
import {
|
import {
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Nav,
|
Nav,
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
NavbarBrand
|
NavbarBrand
|
||||||
} from 'react-bootstrap';
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
|
import { Link } from '../Router';
|
||||||
import navLinks from './links.json';
|
import navLinks from './links.json';
|
||||||
import SignUp from './Sign-Up.jsx';
|
import SignUp from './Sign-Up.jsx';
|
||||||
import BinButton from './Bin-Button.jsx';
|
import BinButton from './Bin-Button.jsx';
|
||||||
@ -28,35 +29,36 @@ import {
|
|||||||
} from './redux';
|
} from './redux';
|
||||||
import {
|
import {
|
||||||
userSelector,
|
userSelector,
|
||||||
|
isSignedInSelector,
|
||||||
signInLoadingSelector
|
signInLoadingSelector
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
import { nameToTypeSelector, panesSelector } from '../Panes/redux';
|
import { panesSelector } from '../Panes/redux';
|
||||||
|
|
||||||
|
|
||||||
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
userSelector,
|
userSelector,
|
||||||
|
isSignedInSelector,
|
||||||
dropdownSelector,
|
dropdownSelector,
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
panesSelector,
|
panesSelector,
|
||||||
nameToTypeSelector,
|
|
||||||
(
|
(
|
||||||
{ username, picture, points },
|
{ username, picture, points },
|
||||||
|
isSignedIn,
|
||||||
isDropdownOpen,
|
isDropdownOpen,
|
||||||
showLoading,
|
showLoading,
|
||||||
panes,
|
panes,
|
||||||
nameToType
|
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
panes: panes.map(name => {
|
panes: panes.map(({ name, type }) => {
|
||||||
return {
|
return {
|
||||||
content: name,
|
content: name,
|
||||||
action: nameToType[name]
|
action: type
|
||||||
};
|
};
|
||||||
}, {}),
|
}, {}),
|
||||||
isDropdownOpen,
|
isDropdownOpen,
|
||||||
isSignedIn: !!username,
|
isSignedIn,
|
||||||
picture,
|
picture,
|
||||||
points,
|
points,
|
||||||
showLoading,
|
showLoading,
|
||||||
@ -104,25 +106,19 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const propTypes = navLinks.reduce(
|
const propTypes = {
|
||||||
(pt, { content }) => {
|
|
||||||
const handler = `handle${capitalize(content)}Click`;
|
|
||||||
pt[handler] = PropTypes.func.isRequired;
|
|
||||||
return pt;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
panes: PropTypes.array,
|
|
||||||
clickOnLogo: PropTypes.func.isRequired,
|
clickOnLogo: PropTypes.func.isRequired,
|
||||||
|
clickOnMap: PropTypes.func.isRequired,
|
||||||
closeDropdown: PropTypes.func.isRequired,
|
closeDropdown: PropTypes.func.isRequired,
|
||||||
isDropdownOpen: PropTypes.bool,
|
isDropdownOpen: PropTypes.bool,
|
||||||
openDropdown: PropTypes.func.isRequired,
|
openDropdown: PropTypes.func.isRequired,
|
||||||
|
panes: PropTypes.array,
|
||||||
picture: PropTypes.string,
|
picture: PropTypes.string,
|
||||||
points: PropTypes.number,
|
points: PropTypes.number,
|
||||||
showLoading: PropTypes.bool,
|
showLoading: PropTypes.bool,
|
||||||
signedIn: PropTypes.bool,
|
signedIn: PropTypes.bool,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
}
|
};
|
||||||
);
|
|
||||||
|
|
||||||
export class FCCNav extends React.Component {
|
export class FCCNav extends React.Component {
|
||||||
renderLink(isNavItem, { isReact, isDropdown, content, link, links, target }) {
|
renderLink(isNavItem, { isReact, isDropdown, content, link, links, target }) {
|
||||||
@ -154,7 +150,7 @@ export class FCCNav extends React.Component {
|
|||||||
}
|
}
|
||||||
if (isReact) {
|
if (isReact) {
|
||||||
return (
|
return (
|
||||||
<LinkContainer
|
<Link
|
||||||
key={ content }
|
key={ content }
|
||||||
onClick={ this.props[`handle${content}Click`] }
|
onClick={ this.props[`handle${content}Click`] }
|
||||||
to={ link }
|
to={ link }
|
||||||
@ -164,7 +160,7 @@ export class FCCNav extends React.Component {
|
|||||||
>
|
>
|
||||||
{ content }
|
{ content }
|
||||||
</Component>
|
</Component>
|
||||||
</LinkContainer>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router';
|
import PropTypes from 'prop-types';
|
||||||
import { NavItem } from 'react-bootstrap';
|
import { NavItem } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import { Link } from '../Router';
|
||||||
|
import { onRouteSettings } from '../routes/Settings/redux';
|
||||||
|
|
||||||
// this is separated out to prevent react bootstrap's
|
// this is separated out to prevent react bootstrap's
|
||||||
// NavBar from injecting unknown props to the li component
|
// NavBar from injecting unknown props to the li component
|
||||||
|
|
||||||
@ -36,7 +39,7 @@ export default function SignUpButton({
|
|||||||
className='nav-avatar'
|
className='nav-avatar'
|
||||||
key='user'
|
key='user'
|
||||||
>
|
>
|
||||||
<Link to='/settings'>
|
<Link to={ onRouteSettings() }>
|
||||||
<span className='nav-username hidden-sm'> { username } </span>
|
<span className='nav-username hidden-sm'> { username } </span>
|
||||||
<span className='nav-points'> [ { points || 1 } ] </span>
|
<span className='nav-points'> [ { points || 1 } ] </span>
|
||||||
<span className='nav-picture-container'>
|
<span className='nav-picture-container'>
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
import { createTypes } from 'redux-create-types';
|
|
||||||
import { createAction, handleActions } from 'redux-actions';
|
|
||||||
import noop from 'lodash/noop';
|
import noop from 'lodash/noop';
|
||||||
|
import {
|
||||||
|
createAction,
|
||||||
|
createTypes,
|
||||||
|
handleActions
|
||||||
|
} from 'berkeleys-redux-utils';
|
||||||
|
|
||||||
import loadCurrentChallengeEpic from './load-current-challenge-epic.js';
|
import loadCurrentChallengeEpic from './load-current-challenge-epic.js';
|
||||||
import binEpic from './bin-epic.js';
|
import binEpic from './bin-epic.js';
|
||||||
@ -62,8 +65,8 @@ const initialState = {
|
|||||||
|
|
||||||
export const dropdownSelector = state => state[ns].isDropdownOpen;
|
export const dropdownSelector = state => state[ns].isDropdownOpen;
|
||||||
|
|
||||||
export default function createReducer() {
|
export default handleActions(
|
||||||
const reducer = handleActions({
|
() => ({
|
||||||
[types.closeDropdown]: state => ({
|
[types.closeDropdown]: state => ({
|
||||||
...state,
|
...state,
|
||||||
isDropdownOpen: false
|
isDropdownOpen: false
|
||||||
@ -72,8 +75,7 @@ export default function createReducer() {
|
|||||||
...state,
|
...state,
|
||||||
isDropdownOpen: true
|
isDropdownOpen: true
|
||||||
})
|
})
|
||||||
}, initialState);
|
}),
|
||||||
|
initialState,
|
||||||
reducer.toString = () => ns;
|
ns
|
||||||
return reducer;
|
);
|
||||||
}
|
|
||||||
|
@ -2,12 +2,11 @@ import { ofType } from 'redux-epic';
|
|||||||
|
|
||||||
import { types } from './';
|
import { types } from './';
|
||||||
import {
|
import {
|
||||||
updateCurrentChallenge,
|
|
||||||
|
|
||||||
userSelector,
|
userSelector,
|
||||||
firstChallengeSelector,
|
firstChallengeSelector,
|
||||||
challengeSelector
|
challengeSelector
|
||||||
} from '../../redux';
|
} from '../../redux';
|
||||||
|
import { onRouteChallenges } from '../../routes/Challenges/redux';
|
||||||
import { entitiesSelector } from '../../entities';
|
import { entitiesSelector } from '../../entities';
|
||||||
|
|
||||||
export default function loadCurrentChallengeEpic(actions, { getState }) {
|
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.
|
// don't reload if the challenge is already loaded.
|
||||||
// This may change to toast to avoid user confusion
|
// This may change to toast to avoid user confusion
|
||||||
))
|
))
|
||||||
.map(({ finalChallenge }) => {
|
.map(({ finalChallenge }) => onRouteChallenges(finalChallenge));
|
||||||
return updateCurrentChallenge(finalChallenge.dashedName);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,15 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { hardGoTo } from '../redux';
|
|
||||||
|
|
||||||
const propTypes = {
|
// import PropTypes from 'prop-types';
|
||||||
hardGoTo: PropTypes.func,
|
|
||||||
location: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
export class NotFound extends React.Component {
|
const propTypes = {};
|
||||||
componentWillMount() {
|
|
||||||
this.props.hardGoTo(this.props.location.pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
export default function NotFound() {
|
||||||
return <span />;
|
return (
|
||||||
}
|
<div>404 Not Found</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotFound.displayName = 'NotFound';
|
NotFound.displayName = 'NotFound';
|
||||||
NotFound.propTypes = propTypes;
|
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 { connect } from 'react-redux';
|
||||||
|
|
||||||
import { dividerClicked } from './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';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
const mapStateToProps = null;
|
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 { connect } from 'react-redux';
|
||||||
|
|
||||||
import Panes from './Panes.jsx';
|
import Panes from './Panes.jsx';
|
||||||
import {
|
import { panesMounted } from './redux';
|
||||||
panesMounted,
|
|
||||||
panesUpdated,
|
|
||||||
panesWillMount,
|
|
||||||
panesWillUnmount
|
|
||||||
} from './redux';
|
|
||||||
|
|
||||||
const mapStateToProps = null;
|
const mapStateToProps = null;
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
panesMounted,
|
panesMounted
|
||||||
panesUpdated,
|
|
||||||
panesWillMount,
|
|
||||||
panesWillUnmount
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
nameToComponent: PropTypes.object.isRequired,
|
nameToComponent: PropTypes.object.isRequired,
|
||||||
panesMounted: PropTypes.func.isRequired,
|
panesMounted: PropTypes.func.isRequired
|
||||||
panesUpdated: PropTypes.func.isRequired,
|
|
||||||
panesWillMount: PropTypes.func.isRequired,
|
|
||||||
panesWillUnmount: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PanesContainer extends PureComponent {
|
export class PanesContainer extends PureComponent {
|
||||||
componentWillMount() {
|
|
||||||
this.props.panesWillMount(Object.keys(this.props.nameToComponent));
|
|
||||||
}
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.panesMounted();
|
this.props.panesMounted();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.panesWillUnmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.nameToComponent !== this.props.nameToComponent) {
|
|
||||||
this.props.panesUpdated(Object.keys(nextProps.nameToComponent));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Panes { ...this.props } />
|
<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 { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ const mapStateToProps = createSelector(
|
|||||||
let lastDividerPosition = 0;
|
let lastDividerPosition = 0;
|
||||||
return {
|
return {
|
||||||
panes: panes
|
panes: panes
|
||||||
.map(name => panesByName[name])
|
.map(({ name }) => panesByName[name])
|
||||||
.filter(({ isHidden })=> !isHidden)
|
.filter(({ isHidden })=> !isHidden)
|
||||||
.map((pane, index, { length: numOfPanes }) => {
|
.map((pane, index, { length: numOfPanes }) => {
|
||||||
const dividerLeft = pane.dividerLeft || 0;
|
const dividerLeft = pane.dividerLeft || 0;
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import { combineActions, createAction, handleActions } from 'redux-actions';
|
import { isLocationAction } from 'redux-first-router';
|
||||||
import { createTypes } from 'redux-create-types';
|
import _ from 'lodash';
|
||||||
import clamp from 'lodash/clamp';
|
import {
|
||||||
|
composeReducers,
|
||||||
|
createAction,
|
||||||
|
createTypes,
|
||||||
|
handleActions
|
||||||
|
} from 'berkeleys-redux-utils';
|
||||||
|
|
||||||
import ns from '../ns.json';
|
import ns from '../ns.json';
|
||||||
|
|
||||||
import windowEpic from './window-epic.js';
|
import windowEpic from './window-epic.js';
|
||||||
import dividerEpic from './divider-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';
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
export const epics = [
|
export const epics = [
|
||||||
@ -14,6 +21,7 @@ export const epics = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const types = createTypes([
|
export const types = createTypes([
|
||||||
|
'panesUpdatedThroughFetch',
|
||||||
'panesMounted',
|
'panesMounted',
|
||||||
'panesUpdated',
|
'panesUpdated',
|
||||||
'panesWillMount',
|
'panesWillMount',
|
||||||
@ -30,6 +38,11 @@ export const types = createTypes([
|
|||||||
'hidePane'
|
'hidePane'
|
||||||
], ns);
|
], ns);
|
||||||
|
|
||||||
|
export const panesUpdatedThroughFetch = createAction(
|
||||||
|
types.panesUpdatedThroughFetch,
|
||||||
|
null,
|
||||||
|
panesView => ({ panesView })
|
||||||
|
);
|
||||||
export const panesMounted = createAction(types.panesMounted);
|
export const panesMounted = createAction(types.panesMounted);
|
||||||
export const panesUpdated = createAction(types.panesUpdated);
|
export const panesUpdated = createAction(types.panesUpdated);
|
||||||
export const panesWillMount = createAction(types.panesWillMount);
|
export const panesWillMount = createAction(types.panesWillMount);
|
||||||
@ -78,9 +91,45 @@ function getDividerLeft(numOfPanes, index) {
|
|||||||
return dividerLeft;
|
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) {
|
if (isDev) {
|
||||||
Object.keys(typeToName).forEach(actionType => {
|
forEachConfig(config, (typeToName, actionType) => {
|
||||||
if (actionType === 'undefined') {
|
if (actionType === 'undefined') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`action type for ${typeToName[actionType]} is undefined`
|
`action type for ${typeToName[actionType]} is undefined`
|
||||||
@ -88,18 +137,24 @@ export default function createPanesAspects(typeToName) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const nameToType = Object.keys(typeToName).reduce((map, type) => {
|
const typeToName = reduceConfig(config, (acc, val, type) => {
|
||||||
map[typeToName[type]] = type;
|
const name = _.isObject(val) ? val.name : val;
|
||||||
return map;
|
acc[type] = name;
|
||||||
}, {});
|
return acc;
|
||||||
function getInitialState() {
|
});
|
||||||
return {
|
|
||||||
...initialState,
|
|
||||||
nameToType
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 => {
|
return next => action => {
|
||||||
let finalAction = action;
|
let finalAction = action;
|
||||||
if (isPanesAction(action, typeToName)) {
|
if (isPanesAction(action, typeToName)) {
|
||||||
@ -107,15 +162,36 @@ export default function createPanesAspects(typeToName) {
|
|||||||
...action,
|
...action,
|
||||||
meta: {
|
meta: {
|
||||||
...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({
|
const reducer = composeReducers(
|
||||||
|
ns,
|
||||||
|
handleActions(
|
||||||
|
() => ({
|
||||||
[types.dividerClicked]: (state, { payload: name }) => ({
|
[types.dividerClicked]: (state, { payload: name }) => ({
|
||||||
...state,
|
...state,
|
||||||
pressedDivider: name
|
pressedDivider: name
|
||||||
@ -123,13 +199,16 @@ export default function createPanesAspects(typeToName) {
|
|||||||
[types.dividerMoved]: (state, { payload: clientX }) => {
|
[types.dividerMoved]: (state, { payload: clientX }) => {
|
||||||
const { width, pressedDivider: paneName } = state;
|
const { width, pressedDivider: paneName } = state;
|
||||||
const dividerBuffer = (200 / width) * 100;
|
const dividerBuffer = (200 / width) * 100;
|
||||||
const paneIndex = state.panes.indexOf(paneName);
|
const paneIndex =
|
||||||
|
_.findIndex(state.panes, ({ name }) => paneName === name);
|
||||||
const currentPane = state.panesByName[paneName];
|
const currentPane = state.panesByName[paneName];
|
||||||
const rightPane = state.panesByName[state.panes[paneIndex + 1]] || {};
|
const rightPane =
|
||||||
const leftPane = state.panesByName[state.panes[paneIndex - 1]] || {};
|
state.panesByName[getPaneName(state.panes, paneIndex + 1)] || {};
|
||||||
|
const leftPane =
|
||||||
|
state.panesByName[getPaneName(state.panes, paneIndex - 1)] || {};
|
||||||
const rightBound = (rightPane.dividerLeft || 100) - dividerBuffer;
|
const rightBound = (rightPane.dividerLeft || 100) - dividerBuffer;
|
||||||
const leftBound = (leftPane.dividerLeft || 0) + dividerBuffer;
|
const leftBound = (leftPane.dividerLeft || 0) + dividerBuffer;
|
||||||
const newPosition = clamp(
|
const newPosition = _.clamp(
|
||||||
(clientX / width) * 100,
|
(clientX / width) * 100,
|
||||||
leftBound,
|
leftBound,
|
||||||
rightBound
|
rightBound
|
||||||
@ -158,17 +237,22 @@ export default function createPanesAspects(typeToName) {
|
|||||||
panesByName: {},
|
panesByName: {},
|
||||||
pressedDivider: null
|
pressedDivider: null
|
||||||
}),
|
}),
|
||||||
[
|
[types.updateNavHeight]: (state, { payload: navHeight }) => ({
|
||||||
combineActions(
|
...state,
|
||||||
panesWillMount,
|
navHeight
|
||||||
panesUpdated
|
})
|
||||||
)
|
}),
|
||||||
]: (state, { payload: panes }) => {
|
initialState,
|
||||||
const numOfPanes = panes.length;
|
),
|
||||||
|
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 {
|
return {
|
||||||
...state,
|
...state,
|
||||||
panes,
|
panes,
|
||||||
panesByName: panes.reduce((panes, name, index) => {
|
panesByName: panes.reduce((panes, { name }, index) => {
|
||||||
const dividerLeft = getDividerLeft(numOfPanes, index);
|
const dividerLeft = getDividerLeft(numOfPanes, index);
|
||||||
panes[name] = {
|
panes[name] = {
|
||||||
name,
|
name,
|
||||||
@ -178,15 +262,9 @@ export default function createPanesAspects(typeToName) {
|
|||||||
return panes;
|
return panes;
|
||||||
}, {})
|
}, {})
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
[types.updateNavHeight]: (state, { payload: navHeight }) => ({
|
|
||||||
...state,
|
|
||||||
navHeight
|
|
||||||
})
|
|
||||||
}, getInitialState());
|
|
||||||
function metaReducer(state = getInitialState(), action) {
|
|
||||||
if (action.meta && action.meta.isPaneAction) {
|
if (action.meta && action.meta.isPaneAction) {
|
||||||
const name = typeToName[action.type];
|
const name = action.meta.paneName;
|
||||||
const oldPane = state.panesByName[name];
|
const oldPane = state.panesByName[name];
|
||||||
const pane = {
|
const pane = {
|
||||||
...oldPane,
|
...oldPane,
|
||||||
@ -196,14 +274,14 @@ export default function createPanesAspects(typeToName) {
|
|||||||
...state.panesByName,
|
...state.panesByName,
|
||||||
[name]: pane
|
[name]: pane
|
||||||
};
|
};
|
||||||
const numOfPanes = state.panes.reduce((sum, name) => {
|
const numOfPanes = state.panes.reduce((sum, { name }) => {
|
||||||
return panesByName[name].isHidden ? sum : sum + 1;
|
return panesByName[name].isHidden ? sum : sum + 1;
|
||||||
}, 0);
|
}, 0);
|
||||||
let numOfHidden = 0;
|
let numOfHidden = 0;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
panesByName: state.panes.reduce(
|
panesByName: state.panes.reduce(
|
||||||
(panesByName, name, index) => {
|
(panesByName, { name }, index) => {
|
||||||
if (!panesByName[name].isHidden) {
|
if (!panesByName[name].isHidden) {
|
||||||
const dividerLeft = getDividerLeft(
|
const dividerLeft = getDividerLeft(
|
||||||
numOfPanes,
|
numOfPanes,
|
||||||
@ -224,13 +302,10 @@ export default function createPanesAspects(typeToName) {
|
|||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function finalReducer(state, action) {
|
|
||||||
return reducer(metaReducer(state, action), action);
|
|
||||||
}
|
|
||||||
finalReducer.toString = () => ns;
|
|
||||||
return {
|
return {
|
||||||
reducer: finalReducer,
|
reducer,
|
||||||
middleware
|
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 { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { NotificationStack } from 'react-notification';
|
import { NotificationStack } from 'react-notification';
|
||||||
@ -6,12 +7,12 @@ import { NotificationStack } from 'react-notification';
|
|||||||
import { removeToast } from './redux';
|
import { removeToast } from './redux';
|
||||||
import {
|
import {
|
||||||
submitChallenge,
|
submitChallenge,
|
||||||
resetChallenge
|
clickOnReset
|
||||||
} from '../routes/challenges/redux';
|
} from '../routes/Challenges/redux';
|
||||||
|
|
||||||
const registeredActions = {
|
const registeredActions = {
|
||||||
submitChallenge,
|
submitChallenge,
|
||||||
resetChallenge
|
clickOnReset
|
||||||
};
|
};
|
||||||
const mapStateToProps = state => ({ toasts: state.toasts });
|
const mapStateToProps = state => ({ toasts: state.toasts });
|
||||||
// we use styles here to overwrite those built into the library
|
// we use styles here to overwrite those built into the library
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { createTypes } from 'redux-create-types';
|
import {
|
||||||
import { createAction, handleActions } from 'redux-actions';
|
createAction,
|
||||||
|
createTypes,
|
||||||
|
handleActions
|
||||||
|
} from 'berkeleys-redux-utils';
|
||||||
|
|
||||||
import ns from '../ns.json';
|
import ns from '../ns.json';
|
||||||
|
|
||||||
export const types = createTypes([
|
export const types = createTypes([
|
||||||
'makeToast',
|
'makeToast',
|
||||||
'removeToast'
|
'removeToast'
|
||||||
], 'toast');
|
], ns);
|
||||||
|
|
||||||
let key = 0;
|
let key = 0;
|
||||||
export const makeToast = createAction(
|
export const makeToast = createAction(
|
||||||
@ -29,8 +32,8 @@ export const removeToast = createAction(
|
|||||||
|
|
||||||
const initialState = [];
|
const initialState = [];
|
||||||
|
|
||||||
export default function createReducer() {
|
export default handleActions(
|
||||||
const reducer = handleActions({
|
() => ({
|
||||||
[types.makeToast]: (state, { payload: toast }) => [
|
[types.makeToast]: (state, { payload: toast }) => [
|
||||||
...state,
|
...state,
|
||||||
toast
|
toast
|
||||||
@ -38,8 +41,7 @@ export default function createReducer() {
|
|||||||
[types.removeToast]: (state, { payload: key }) => state.filter(
|
[types.removeToast]: (state, { payload: key }) => state.filter(
|
||||||
toast => toast.key !== key
|
toast => toast.key !== key
|
||||||
)
|
)
|
||||||
}, initialState);
|
}),
|
||||||
|
initialState,
|
||||||
reducer.toString = () => ns;
|
ns
|
||||||
return reducer;
|
);
|
||||||
}
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
// Here we invert the order in which
|
// Here we invert the order in which
|
||||||
// they are painted using css so the
|
// they are painted using css so the
|
||||||
// nav is on top again
|
// nav is on top again
|
||||||
.grid(@direction: column-reverse);
|
.grid(@direction: column);
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{ns}-content {
|
.@{ns}-content {
|
||||||
|
@ -1,42 +1,35 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import { match } from 'react-router';
|
|
||||||
import { compose, createStore, applyMiddleware } from 'redux';
|
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 { createEpic } from 'redux-epic';
|
||||||
import createReducer from './create-reducer';
|
import appReducer from './reducer.js';
|
||||||
import createRoutes from './create-routes.js';
|
import routesMap from './routes-map.js';
|
||||||
import createPanesMap from './create-panes-map.js';
|
import createPanesMap from './create-panes-map.js';
|
||||||
import createPanesAspects from './Panes/redux';
|
import createPanesAspects from './Panes/redux';
|
||||||
|
import addLangToRoutesEnhancer from './Router/redux/add-lang-enhancer.js';
|
||||||
import epics from './epics';
|
import epics from './epics';
|
||||||
|
|
||||||
|
import { onBeforeChange } from './utils/redux-first-router.js';
|
||||||
import servicesCreator from '../utils/services-creator';
|
import servicesCreator from '../utils/services-creator';
|
||||||
|
|
||||||
const createRouteProps = Observable.fromNodeCallback(match);
|
|
||||||
|
|
||||||
//
|
|
||||||
// createApp(settings: {
|
// createApp(settings: {
|
||||||
// location?: Location|String,
|
|
||||||
// history?: History,
|
// history?: History,
|
||||||
// syncHistoryWithStore?: ((history, store) => history) = (x) => x,
|
// defaultState?: Object|Void,
|
||||||
// initialState?: Object|Void,
|
|
||||||
// serviceOptions?: Object,
|
// serviceOptions?: Object,
|
||||||
// middlewares?: Function[],
|
// middlewares?: Function[],
|
||||||
// sideReducers?: Object
|
|
||||||
// enhancers?: Function[],
|
// enhancers?: Function[],
|
||||||
// epics?: Function[],
|
// epics?: Function[],
|
||||||
// }) => Observable
|
// }) => Observable
|
||||||
//
|
//
|
||||||
// Either location or history must be defined
|
// Either location or history must be defined
|
||||||
export default function createApp({
|
export default function createApp({
|
||||||
location,
|
|
||||||
history,
|
history,
|
||||||
syncHistoryWithStore = (x) => x,
|
defaultState,
|
||||||
syncOptions = {},
|
|
||||||
initialState,
|
|
||||||
serviceOptions = {},
|
serviceOptions = {},
|
||||||
middlewares: sideMiddlewares = [],
|
middlewares: sideMiddlewares = [],
|
||||||
enhancers: sideEnhancers = [],
|
enhancers: sideEnhancers = [],
|
||||||
reducers: sideReducers = {},
|
|
||||||
epics: sideEpics = [],
|
epics: sideEpics = [],
|
||||||
epicOptions: sideEpicOptions = {}
|
epicOptions: sideEpicOptions = {}
|
||||||
}) {
|
}) {
|
||||||
@ -50,12 +43,25 @@ export default function createApp({
|
|||||||
...epics,
|
...epics,
|
||||||
...sideEpics
|
...sideEpics
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
reducer: routesReducer,
|
||||||
|
middleware: routesMiddleware,
|
||||||
|
enhancer: routesEnhancer
|
||||||
|
} = connectRoutes(history, routesMap, { onBeforeChange });
|
||||||
|
|
||||||
|
routesReducer.toString = () => 'location';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
reducer: panesReducer,
|
reducer: panesReducer,
|
||||||
middleware: panesMiddleware
|
middleware: panesMiddleware
|
||||||
} = createPanesAspects(createPanesMap());
|
} = createPanesAspects(createPanesMap());
|
||||||
|
|
||||||
const enhancer = compose(
|
const enhancer = compose(
|
||||||
|
addLangToRoutesEnhancer(routesMap),
|
||||||
|
routesEnhancer,
|
||||||
applyMiddleware(
|
applyMiddleware(
|
||||||
|
routesMiddleware,
|
||||||
panesMiddleware,
|
panesMiddleware,
|
||||||
epicMiddleware,
|
epicMiddleware,
|
||||||
...sideMiddlewares
|
...sideMiddlewares
|
||||||
@ -64,33 +70,31 @@ export default function createApp({
|
|||||||
// on client side these are things like Redux DevTools
|
// on client side these are things like Redux DevTools
|
||||||
...sideEnhancers
|
...sideEnhancers
|
||||||
);
|
);
|
||||||
const reducer = createReducer(
|
|
||||||
{
|
const reducer = combineReducers(
|
||||||
[panesReducer]: panesReducer,
|
appReducer,
|
||||||
...sideReducers
|
panesReducer,
|
||||||
},
|
routesReducer
|
||||||
);
|
);
|
||||||
|
|
||||||
// create composed store enhancer
|
// create composed store enhancer
|
||||||
// use store enhancer function to enhance `createStore` function
|
// 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
|
// to create store
|
||||||
const store = createStore(reducer, initialState, enhancer);
|
const store = createStore(reducer, defaultState, enhancer);
|
||||||
// sync history client side with store.
|
const location = selectLocationState(store.getState());
|
||||||
// server side this is an identity function and history is undefined
|
|
||||||
history = syncHistoryWithStore(history, store, syncOptions);
|
// ({
|
||||||
const routes = createRoutes(store);
|
// redirect,
|
||||||
// createRouteProps({
|
// props,
|
||||||
// redirect: LocationDescriptor,
|
// reducer,
|
||||||
// history: History,
|
// store,
|
||||||
// routes: Object
|
// epic: epicMiddleware
|
||||||
// }) => Observable
|
// }));
|
||||||
return createRouteProps({ routes, location, history })
|
return Observable.of({
|
||||||
.map(([ redirect, props ]) => ({
|
|
||||||
redirect,
|
|
||||||
props,
|
|
||||||
reducer,
|
|
||||||
store,
|
store,
|
||||||
epic: epicMiddleware
|
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 {
|
||||||
import { createAction, handleActions } from 'redux-actions';
|
composeReducers,
|
||||||
|
createAction,
|
||||||
|
createTypes,
|
||||||
|
handleActions
|
||||||
|
} from 'berkeleys-redux-utils';
|
||||||
|
|
||||||
|
import { types as app } from '../routes/Challenges/redux';
|
||||||
|
|
||||||
export const ns = 'entities';
|
export const ns = 'entities';
|
||||||
export const getNS = state => state[ns];
|
export const getNS = state => state[ns];
|
||||||
export const entitiesSelector = getNS;
|
export const entitiesSelector = getNS;
|
||||||
export const types = createTypes([
|
export const types = createTypes([
|
||||||
'updateUserPoints',
|
|
||||||
'updateUserFlag',
|
'updateUserFlag',
|
||||||
'updateUserEmail',
|
'updateUserEmail',
|
||||||
'updateUserLang',
|
'updateUserLang',
|
||||||
'updateUserChallenge',
|
|
||||||
'updateUserCurrentChallenge'
|
'updateUserCurrentChallenge'
|
||||||
], ns);
|
], ns);
|
||||||
|
|
||||||
// updateUserPoints(username: String, points: Number) => Action
|
|
||||||
export const updateUserPoints = createAction(
|
|
||||||
types.updateUserPoints,
|
|
||||||
(username, points) => ({ username, points })
|
|
||||||
);
|
|
||||||
// updateUserFlag(username: String, flag: String) => Action
|
// updateUserFlag(username: String, flag: String) => Action
|
||||||
export const updateUserFlag = createAction(
|
export const updateUserFlag = createAction(
|
||||||
types.updateUserFlag,
|
types.updateUserFlag,
|
||||||
@ -34,21 +33,12 @@ export const updateUserLang = createAction(
|
|||||||
(username, lang) => ({ username, languageTag: lang })
|
(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(
|
export const updateUserCurrentChallenge = createAction(
|
||||||
types.updateUserCurrentChallenge
|
types.updateUserCurrentChallenge
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const initialState = {
|
const defaultState = {
|
||||||
superBlock: {},
|
superBlock: {},
|
||||||
block: {},
|
block: {},
|
||||||
challenge: {},
|
challenge: {},
|
||||||
@ -69,14 +59,33 @@ export function makeSuperBlockSelector(name) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createReducer() {
|
export const isChallengeLoaded = (state, { dashedName }) =>
|
||||||
const userReducer = handleActions(
|
!!challengeMapSelector(state)[dashedName];
|
||||||
{
|
|
||||||
[types.updateUserPoints]: (state, { payload: { username, points } }) => ({
|
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,
|
...state,
|
||||||
[username]: {
|
[username]: {
|
||||||
...state[username],
|
...state[username],
|
||||||
points
|
points,
|
||||||
|
challengeMap: {
|
||||||
|
...state[username].challengeMap,
|
||||||
|
[challengeInfo.id]: challengeInfo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[types.updateUserFlag]: (state, { payload: { username, flag } }) => ({
|
[types.updateUserFlag]: (state, { payload: { username, flag } }) => ({
|
||||||
@ -118,46 +127,8 @@ export default function createReducer() {
|
|||||||
...state[username],
|
...state[username],
|
||||||
currentChallengeId
|
currentChallengeId
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
[types.updateUserChallenge]:
|
|
||||||
(
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
payload: { username, challengeInfo }
|
|
||||||
}
|
|
||||||
) => ({
|
|
||||||
...state,
|
|
||||||
[username]: {
|
|
||||||
...state[username],
|
|
||||||
challengeMap: {
|
|
||||||
...state[username].challengeMap,
|
|
||||||
[challengeInfo.id]: challengeInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
}),
|
||||||
initialState.user
|
defaultState
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { epics as app } from './redux';
|
import { epics as app } from './redux';
|
||||||
import { epics as challenge } from './routes/challenges/redux';
|
import { epics as challenge } from './routes/Challenges/redux';
|
||||||
import { epics as settings } from './routes/settings/redux';
|
import { epics as settings } from './routes/Settings/redux';
|
||||||
import { epics as nav } from './Nav/redux';
|
import { epics as nav } from './Nav/redux';
|
||||||
import { epics as map } from './Map/redux';
|
import { epics as map } from './Map/redux';
|
||||||
import { epics as panes } from './Panes/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 */
|
/* eslint-disable react/display-name */
|
||||||
import React from 'react';
|
import { createElement } from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
export default function provideStore(element, store) {
|
export default function provideStore(Component, store) {
|
||||||
return React.createElement(
|
return createElement(
|
||||||
Provider,
|
Provider,
|
||||||
{ store },
|
{ 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,
|
delayedRedirect,
|
||||||
|
|
||||||
fetchChallengeCompleted,
|
fetchChallengeCompleted,
|
||||||
fetchChallengesCompleted,
|
fetchChallengesCompleted
|
||||||
|
|
||||||
langSelector
|
|
||||||
} from './';
|
} from './';
|
||||||
|
import { isChallengeLoaded } from '../entities/index.js';
|
||||||
|
|
||||||
import { shapeChallenges } from './utils';
|
import { shapeChallenges } from './utils';
|
||||||
|
import { types as challenge } from '../routes/Challenges/redux';
|
||||||
|
import { langSelector } from '../Router/redux';
|
||||||
|
|
||||||
const isDev = debug.enabled('fcc:*');
|
const isDev = debug.enabled('fcc:*');
|
||||||
|
|
||||||
export function fetchChallengeEpic(actions, { getState }, { services }) {
|
export function fetchChallengeEpic(actions, { getState }, { services }) {
|
||||||
return actions::ofType('' + types.fetchChallenge)
|
return actions::ofType(challenge.onRouteChallenges)
|
||||||
.flatMap(({ payload: { dashedName, block } }) => {
|
.filter(({ payload }) => !isChallengeLoaded(getState(), payload))
|
||||||
const lang = langSelector(getState());
|
.flatMapLatest(({ payload: params }) => {
|
||||||
const options = {
|
const options = {
|
||||||
service: 'map',
|
service: 'map',
|
||||||
params: { block, dashedName, lang }
|
params
|
||||||
};
|
};
|
||||||
return services.readService$(options)
|
return services.readService$(options)
|
||||||
.retry(3)
|
.retry(3)
|
||||||
@ -52,11 +54,7 @@ export function fetchChallengesEpic(
|
|||||||
{ getState },
|
{ getState },
|
||||||
{ services }
|
{ services }
|
||||||
) {
|
) {
|
||||||
return actions::ofType(
|
return actions::ofType(types.appMounted)
|
||||||
// async type
|
|
||||||
'' + types.fetchChallenges,
|
|
||||||
types.appMounted
|
|
||||||
)
|
|
||||||
.flatMapLatest(() => {
|
.flatMapLatest(() => {
|
||||||
const lang = langSelector(getState());
|
const lang = langSelector(getState());
|
||||||
const options = {
|
const options = {
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
addThemeToBody
|
addThemeToBody
|
||||||
} from './';
|
} from './';
|
||||||
|
|
||||||
export default function getUserEpic(actions, { getState }, { services }) {
|
export default function getUserEpic(actions, _, { services }) {
|
||||||
return actions::ofType(types.fetchUser)
|
return actions::ofType(types.fetchUser)
|
||||||
.flatMap(() => {
|
.flatMap(() => {
|
||||||
return services.readService$({ service: 'user' })
|
return services.readService$({ service: 'user' })
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import { createTypes, createAsyncTypes } from 'redux-create-types';
|
import {
|
||||||
import { combineActions, createAction, handleActions } from 'redux-actions';
|
combineActions,
|
||||||
|
createAction,
|
||||||
|
createAsyncTypes,
|
||||||
|
createTypes,
|
||||||
|
handleActions
|
||||||
|
} from 'berkeleys-redux-utils';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import noop from 'lodash/noop';
|
import noop from 'lodash/noop';
|
||||||
import identity from 'lodash/identity';
|
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 updateMyCurrentChallengeEpic from './update-my-challenge-epic.js';
|
||||||
import fetchChallengesEpic from './fetch-challenges-epic.js';
|
import fetchChallengesEpic from './fetch-challenges-epic.js';
|
||||||
import navSizeEpic from './nav-size-epic.js';
|
import navSizeEpic from './nav-size-epic.js';
|
||||||
|
import { types as challenges } from '../routes/Challenges/redux';
|
||||||
|
|
||||||
import ns from '../ns.json';
|
import ns from '../ns.json';
|
||||||
|
|
||||||
@ -21,14 +27,14 @@ export const epics = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const types = createTypes([
|
export const types = createTypes([
|
||||||
|
'onRouteHome',
|
||||||
|
|
||||||
'appMounted',
|
'appMounted',
|
||||||
'analytics',
|
'analytics',
|
||||||
'updateTitle',
|
'updateTitle',
|
||||||
'updateAppLang',
|
|
||||||
|
|
||||||
createAsyncTypes('fetchChallenge'),
|
createAsyncTypes('fetchChallenge'),
|
||||||
createAsyncTypes('fetchChallenges'),
|
createAsyncTypes('fetchChallenges'),
|
||||||
'updateCurrentChallenge',
|
|
||||||
|
|
||||||
'fetchUser',
|
'fetchUser',
|
||||||
'addUser',
|
'addUser',
|
||||||
@ -80,6 +86,7 @@ export const createEventMetaCreator = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const onRouteHome = createAction(types.onRouteHome);
|
||||||
export const appMounted = createAction(types.appMounted);
|
export const appMounted = createAction(types.appMounted);
|
||||||
export const fetchChallenge = createAction(
|
export const fetchChallenge = createAction(
|
||||||
'' + types.fetchChallenge,
|
'' + types.fetchChallenge,
|
||||||
@ -96,9 +103,6 @@ export const fetchChallengesCompleted = createAction(
|
|||||||
(entities, result) => ({ entities, result }),
|
(entities, result) => ({ entities, result }),
|
||||||
entities => ({ entities })
|
entities => ({ entities })
|
||||||
);
|
);
|
||||||
export const updateCurrentChallenge = createAction(
|
|
||||||
types.updateCurrentChallenge
|
|
||||||
);
|
|
||||||
|
|
||||||
// updateTitle(title: String) => Action
|
// updateTitle(title: String) => Action
|
||||||
export const updateTitle = createAction(types.updateTitle);
|
export const updateTitle = createAction(types.updateTitle);
|
||||||
@ -118,8 +122,6 @@ export const addUser = createAction(
|
|||||||
export const updateThisUser = createAction(types.updateThisUser);
|
export const updateThisUser = createAction(types.updateThisUser);
|
||||||
export const showSignIn = createAction(types.showSignIn);
|
export const showSignIn = createAction(types.showSignIn);
|
||||||
|
|
||||||
export const updateAppLang = createAction(types.updateAppLang);
|
|
||||||
|
|
||||||
// used when server needs client to redirect
|
// used when server needs client to redirect
|
||||||
export const delayedRedirect = createAction(types.delayedRedirect);
|
export const delayedRedirect = createAction(types.delayedRedirect);
|
||||||
|
|
||||||
@ -156,7 +158,6 @@ const initialState = {
|
|||||||
title: 'Learn To Code | freeCodeCamp',
|
title: 'Learn To Code | freeCodeCamp',
|
||||||
isSignInAttempted: false,
|
isSignInAttempted: false,
|
||||||
user: '',
|
user: '',
|
||||||
lang: '',
|
|
||||||
csrfToken: '',
|
csrfToken: '',
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
// eventually this should be only in the user object
|
// eventually this should be only in the user object
|
||||||
@ -165,7 +166,6 @@ const initialState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getNS = state => state[ns];
|
export const getNS = state => state[ns];
|
||||||
export const langSelector = state => getNS(state).lang;
|
|
||||||
export const csrfSelector = state => getNS(state).csrfToken;
|
export const csrfSelector = state => getNS(state).csrfToken;
|
||||||
export const themeSelector = state => getNS(state).theme;
|
export const themeSelector = state => getNS(state).theme;
|
||||||
export const titleSelector = state => getNS(state).title;
|
export const titleSelector = state => getNS(state).title;
|
||||||
@ -180,6 +180,8 @@ export const userSelector = createSelector(
|
|||||||
(username, userMap) => userMap[username] || {}
|
(username, userMap) => userMap[username] || {}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const isSignedInSelector = state => !!userSelector(state).username;
|
||||||
|
|
||||||
export const challengeSelector = createSelector(
|
export const challengeSelector = createSelector(
|
||||||
currentChallengeSelector,
|
currentChallengeSelector,
|
||||||
state => entitiesSelector(state).challenge,
|
state => entitiesSelector(state).challenge,
|
||||||
@ -222,9 +224,8 @@ export const firstChallengeSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function createReducer() {
|
export default handleActions(
|
||||||
const reducer = handleActions(
|
() => ({
|
||||||
{
|
|
||||||
[types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
|
[types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
|
||||||
...state,
|
...state,
|
||||||
title: payload + ' | freeCodeCamp'
|
title: payload + ' | freeCodeCamp'
|
||||||
@ -234,10 +235,6 @@ export default function createReducer() {
|
|||||||
...state,
|
...state,
|
||||||
user
|
user
|
||||||
}),
|
}),
|
||||||
[types.fetchChallenge.complete]: (state, { payload }) => ({
|
|
||||||
...state,
|
|
||||||
currentChallenge: payload.currentChallenge
|
|
||||||
}),
|
|
||||||
[combineActions(
|
[combineActions(
|
||||||
types.fetchChallenge.complete,
|
types.fetchChallenge.complete,
|
||||||
types.fetchChallenges.complete
|
types.fetchChallenges.complete
|
||||||
@ -245,13 +242,9 @@ export default function createReducer() {
|
|||||||
...state,
|
...state,
|
||||||
superBlocks: payload.result.superBlocks
|
superBlocks: payload.result.superBlocks
|
||||||
}),
|
}),
|
||||||
[types.updateCurrentChallenge]: (state, { payload = '' }) => ({
|
[challenges.onRouteChallenges]: (state, { payload: { dashedName } }) => ({
|
||||||
...state,
|
...state,
|
||||||
currentChallenge: payload
|
currentChallenge: dashedName
|
||||||
}),
|
|
||||||
[types.updateAppLang]: (state, { payload = 'en' }) =>({
|
|
||||||
...state,
|
|
||||||
lang: payload
|
|
||||||
}),
|
}),
|
||||||
[types.updateTheme]: (state, { payload = 'default' }) => ({
|
[types.updateTheme]: (state, { payload = 'default' }) => ({
|
||||||
...state,
|
...state,
|
||||||
@ -270,10 +263,7 @@ export default function createReducer() {
|
|||||||
...state,
|
...state,
|
||||||
delayedRedirect: payload
|
delayedRedirect: payload
|
||||||
})
|
})
|
||||||
},
|
}),
|
||||||
initialState
|
initialState,
|
||||||
|
ns
|
||||||
);
|
);
|
||||||
|
|
||||||
reducer.toString = () => ns;
|
|
||||||
return reducer;
|
|
||||||
}
|
|
||||||
|
@ -12,12 +12,15 @@ import {
|
|||||||
} from './';
|
} from './';
|
||||||
import { updateUserCurrentChallenge } from '../entities';
|
import { updateUserCurrentChallenge } from '../entities';
|
||||||
import { postJSON$ } from '../../utils/ajax-stream';
|
import { postJSON$ } from '../../utils/ajax-stream';
|
||||||
|
import { types as challenges } from '../routes/Challenges/redux';
|
||||||
|
|
||||||
const log = debug('fcc:app:redux:up-my-challenge-epic');
|
const log = debug('fcc:app:redux:up-my-challenge-epic');
|
||||||
export default function updateMyCurrentChallengeEpic(actions, { getState }) {
|
export default function updateMyCurrentChallengeEpic(actions, { getState }) {
|
||||||
const updateChallenge = actions::ofType(types.updateCurrentChallenge)
|
const updateChallenge = actions::ofType(types.appMounted)
|
||||||
|
.flatMapLatest(() => actions::ofType(challenges.onRouteChallenges))
|
||||||
.map(() => {
|
.map(() => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
// username is never defined SSR
|
||||||
const { username } = userSelector(state);
|
const { username } = userSelector(state);
|
||||||
const { id } = challengeSelector(state);
|
const { id } = challengeSelector(state);
|
||||||
const csrf = csrfSelector(state);
|
const csrf = csrfSelector(state);
|
||||||
|
@ -10,7 +10,7 @@ export function filterComingSoonBetaChallenge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function filterComingSoonBetaFromEntities(
|
export function filterComingSoonBetaFromEntities(
|
||||||
{ challenge: challengeMap, block: blockMap, ...rest },
|
{ challenge: challengeMap, block: blockMap = {}, ...rest },
|
||||||
isDev = false
|
isDev = false
|
||||||
) {
|
) {
|
||||||
const filter = filterComingSoonBetaChallenge.bind(null, isDev);
|
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 { connect } from 'react-redux';
|
||||||
import { Button, Modal } from 'react-bootstrap';
|
import { Button, Modal } from 'react-bootstrap';
|
||||||
import PureComponent from 'react-pure-render/component';
|
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 { Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
import ns from './ns.json';
|
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';
|
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 PureComponent from 'react-pure-render/component';
|
||||||
import { Grid, Col, Row } from 'react-bootstrap';
|
import { Grid, Col, Row } from 'react-bootstrap';
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
import noop from 'lodash/noop';
|
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 { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { Button, Modal } from 'react-bootstrap';
|
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 NoSSR from 'react-no-ssr';
|
||||||
import Codemirror from 'react-codemirror';
|
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 { HelpBlock, FormGroup, FormControl } from 'react-bootstrap';
|
||||||
import { getValidationState, DOMOnlyProps } from '../../utils/form';
|
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 classnames from 'classnames';
|
||||||
import { Col, Row } from 'react-bootstrap';
|
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 { ofType } from 'redux-epic';
|
||||||
import {
|
import {
|
||||||
types,
|
types,
|
||||||
closeBugModal,
|
closeBugModal
|
||||||
|
|
||||||
filesSelector
|
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
|
|
||||||
|
import { filesSelector } from '../../../files';
|
||||||
import { currentChallengeSelector } from '../../../redux';
|
import { currentChallengeSelector } from '../../../redux';
|
||||||
|
|
||||||
function filesToMarkdown(files = {}) {
|
function filesToMarkdown(files = {}) {
|
@ -1,13 +1,14 @@
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import { combineEpics, ofType } from 'redux-epic';
|
import { combineEpics, ofType } from 'redux-epic';
|
||||||
import { push } from 'react-router-redux';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
types,
|
types,
|
||||||
|
|
||||||
updateMain,
|
challengeUpdated,
|
||||||
challengeUpdated
|
onRouteChallenges,
|
||||||
|
onRouteCurrentChallenge,
|
||||||
|
updateMain
|
||||||
} from './';
|
} from './';
|
||||||
import { getNS as entitiesSelector } from '../../../entities';
|
import { getNS as entitiesSelector } from '../../../entities';
|
||||||
import {
|
import {
|
||||||
@ -16,40 +17,42 @@ import {
|
|||||||
getFirstChallengeOfNextSuperBlock
|
getFirstChallengeOfNextSuperBlock
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import {
|
import {
|
||||||
types as app,
|
|
||||||
|
|
||||||
createErrorObservable,
|
createErrorObservable,
|
||||||
updateCurrentChallenge,
|
|
||||||
|
|
||||||
currentChallengeSelector,
|
currentChallengeSelector,
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
superBlocksSelector
|
superBlocksSelector
|
||||||
} from '../../../redux';
|
} from '../../../redux';
|
||||||
|
import { langSelector } from '../../../Router/redux';
|
||||||
import { makeToast } from '../../../Toasts/redux';
|
import { makeToast } from '../../../Toasts/redux';
|
||||||
|
|
||||||
const isDev = debug.enabled('fcc:*');
|
const isDev = debug.enabled('fcc:*');
|
||||||
|
|
||||||
|
// When we change challenge, update the current challenge
|
||||||
|
// UI data.
|
||||||
export function challengeUpdatedEpic(actions, { getState }) {
|
export function challengeUpdatedEpic(actions, { getState }) {
|
||||||
return actions::ofType(app.updateCurrentChallenge)
|
return actions::ofType(types.onRouteChallenges)
|
||||||
.flatMap(() => {
|
// prevent subsequent onRouteChallenges to cause UI to refresh
|
||||||
const challenge = challengeSelector(getState());
|
.distinctUntilChanged(({ payload: { dashedName }}) => dashedName)
|
||||||
return Observable.of(
|
.map(() => challengeSelector(getState()))
|
||||||
challengeUpdated(challenge),
|
// if the challenge isn't loaded in the current state,
|
||||||
push(`/challenges/${challenge.block}/${challenge.dashedName}`)
|
// 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
|
// used to reset users code on request
|
||||||
export function resetChallengeEpic(actions, { getState }) {
|
export function resetChallengeEpic(actions, { getState }) {
|
||||||
return actions::ofType(types.resetChallenge)
|
return actions::ofType(types.clickOnReset)
|
||||||
.flatMap(() => {
|
.flatMap(() =>
|
||||||
const currentChallenge = currentChallengeSelector(getState());
|
Observable.of(
|
||||||
return Observable.of(
|
challengeUpdated(challengeSelector(getState())),
|
||||||
updateCurrentChallenge(currentChallenge),
|
|
||||||
updateMain()
|
updateMain()
|
||||||
);
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nextChallengeEpic(actions, { getState }) {
|
export function nextChallengeEpic(actions, { getState }) {
|
||||||
@ -64,6 +67,7 @@ export function nextChallengeEpic(actions, { getState }) {
|
|||||||
const superBlocks = superBlocksSelector(state);
|
const superBlocks = superBlocksSelector(state);
|
||||||
const challenge = currentChallengeSelector(state);
|
const challenge = currentChallengeSelector(state);
|
||||||
const entities = entitiesSelector(state);
|
const entities = entitiesSelector(state);
|
||||||
|
const lang = langSelector(state);
|
||||||
nextChallenge = getNextChallenge(challenge, entities, { isDev });
|
nextChallenge = getNextChallenge(challenge, entities, { isDev });
|
||||||
// block completed.
|
// block completed.
|
||||||
if (!nextChallenge) {
|
if (!nextChallenge) {
|
||||||
@ -107,11 +111,15 @@ export function nextChallengeEpic(actions, { getState }) {
|
|||||||
'that have not been passed yet. ',
|
'that have not been passed yet. ',
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
}),
|
}),
|
||||||
push('/map')
|
onRouteCurrentChallenge()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Observable.of(
|
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.' })
|
makeToast({ message: 'Your next challenge has arrived.' })
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
@ -2,48 +2,45 @@ import { Observable } from 'rx';
|
|||||||
import { ofType } from 'redux-epic';
|
import { ofType } from 'redux-epic';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
types,
|
|
||||||
|
|
||||||
moveToNextChallenge,
|
|
||||||
clearSavedCode,
|
|
||||||
|
|
||||||
challengeMetaSelector,
|
challengeMetaSelector,
|
||||||
filesSelector,
|
moveToNextChallenge,
|
||||||
testsSelector
|
submitChallengeComplete,
|
||||||
|
testsSelector,
|
||||||
|
types
|
||||||
} from './';
|
} from './';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createErrorObservable,
|
|
||||||
|
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
|
createErrorObservable,
|
||||||
csrfSelector,
|
csrfSelector,
|
||||||
userSelector
|
userSelector
|
||||||
} from '../../../redux';
|
} from '../../../redux';
|
||||||
import {
|
import { filesSelector } from '../../../files';
|
||||||
updateUserPoints,
|
|
||||||
updateUserChallenge
|
|
||||||
} from '../../../entities';
|
|
||||||
import { backEndProject } from '../../../utils/challengeTypes.js';
|
import { backEndProject } from '../../../utils/challengeTypes.js';
|
||||||
import { makeToast } from '../../../Toasts/redux';
|
import { makeToast } from '../../../Toasts/redux';
|
||||||
import { postJSON$ } from '../../../../utils/ajax-stream.js';
|
import { postJSON$ } from '../../../../utils/ajax-stream.js';
|
||||||
|
|
||||||
function postChallenge(url, username, _csrf, challengeInfo) {
|
function postChallenge(url, username, _csrf, challengeInfo) {
|
||||||
|
return Observable.if(
|
||||||
|
() => !!username,
|
||||||
|
Observable.defer(() => {
|
||||||
const body = { ...challengeInfo, _csrf };
|
const body = { ...challengeInfo, _csrf };
|
||||||
const saveChallenge = postJSON$(url, body)
|
const saveChallenge = postJSON$(url, body)
|
||||||
.retry(3)
|
.retry(3)
|
||||||
.flatMap(({ points, lastUpdated, completedDate }) => {
|
.map(({ points, lastUpdated, completedDate }) =>
|
||||||
return Observable.of(
|
submitChallengeComplete(
|
||||||
updateUserPoints(username, points),
|
|
||||||
updateUserChallenge(
|
|
||||||
username,
|
username,
|
||||||
|
points,
|
||||||
{ ...challengeInfo, lastUpdated, completedDate }
|
{ ...challengeInfo, lastUpdated, completedDate }
|
||||||
),
|
)
|
||||||
clearSavedCode()
|
)
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(createErrorObservable);
|
.catch(createErrorObservable);
|
||||||
const challengeCompleted = Observable.of(moveToNextChallenge());
|
const challengeCompleted = Observable.of(moveToNextChallenge());
|
||||||
return Observable.merge(saveChallenge, challengeCompleted);
|
return Observable.merge(saveChallenge, challengeCompleted)
|
||||||
|
.startWith({ type: types.submitChallenge.start });
|
||||||
|
}),
|
||||||
|
Observable.of(moveToNextChallenge())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitModern(type, state) {
|
function submitModern(type, state) {
|
||||||
@ -53,7 +50,7 @@ function submitModern(type, state) {
|
|||||||
return Observable.empty();
|
return Observable.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === types.submitChallenge) {
|
if (type === types.submitChallenge.toString()) {
|
||||||
const { id } = challengeSelector(state);
|
const { id } = challengeSelector(state);
|
||||||
const files = filesSelector(state);
|
const files = filesSelector(state);
|
||||||
const { username } = userSelector(state);
|
const { username } = userSelector(state);
|
||||||
@ -145,7 +142,7 @@ const submitters = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function completionEpic(actions, { getState }) {
|
export default function completionEpic(actions, { getState }) {
|
||||||
return actions::ofType(types.checkChallenge, types.submitChallenge)
|
return actions::ofType(types.checkChallenge, types.submitChallenge.toString())
|
||||||
.flatMap(({ type, payload }) => {
|
.flatMap(({ type, payload }) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { submitType } = challengeMetaSelector(state);
|
const { submitType } = challengeMetaSelector(state);
|
@ -2,11 +2,11 @@ import { ofType } from 'redux-epic';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
types,
|
types,
|
||||||
updateFile,
|
|
||||||
|
|
||||||
keySelector
|
keySelector
|
||||||
} from './';
|
} from './';
|
||||||
|
|
||||||
|
import { updateFile } from '../../../files';
|
||||||
|
|
||||||
export default function editorEpic(actions, { getState }) {
|
export default function editorEpic(actions, { getState }) {
|
||||||
return actions::ofType(types.classicEditorUpdated)
|
return actions::ofType(types.classicEditorUpdated)
|
||||||
.pluck('payload')
|
.pluck('payload')
|
@ -1,5 +1,11 @@
|
|||||||
import { createTypes } from 'redux-create-types';
|
import {
|
||||||
import { createAction, combineActions, handleActions } from 'redux-actions';
|
combineActions,
|
||||||
|
combineReducers,
|
||||||
|
createAction,
|
||||||
|
createAsyncTypes,
|
||||||
|
createTypes,
|
||||||
|
handleActions
|
||||||
|
} from 'berkeleys-redux-utils';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import noop from 'lodash/noop';
|
import noop from 'lodash/noop';
|
||||||
|
|
||||||
@ -10,11 +16,7 @@ import editorEpic from './editor-epic.js';
|
|||||||
|
|
||||||
import ns from '../ns.json';
|
import ns from '../ns.json';
|
||||||
import {
|
import {
|
||||||
arrayToString,
|
|
||||||
buildSeed,
|
|
||||||
createTests,
|
createTests,
|
||||||
getFileKey,
|
|
||||||
getPreFile,
|
|
||||||
loggerToStr,
|
loggerToStr,
|
||||||
submitTypes,
|
submitTypes,
|
||||||
viewTypes
|
viewTypes
|
||||||
@ -23,12 +25,12 @@ import {
|
|||||||
types as app,
|
types as app,
|
||||||
challengeSelector
|
challengeSelector
|
||||||
} from '../../../redux';
|
} from '../../../redux';
|
||||||
import { bonfire, html, js } from '../../../utils/challengeTypes';
|
import { html } from '../../../utils/challengeTypes.js';
|
||||||
import blockNameify from '../../../utils/blockNameify';
|
import blockNameify from '../../../utils/blockNameify.js';
|
||||||
import { createPoly, setContent } from '../../../../utils/polyvinyl';
|
import { getFileKey } from '../../../utils/classic-file.js';
|
||||||
import createStepReducer, { epics as stepEpics } from '../views/step/redux';
|
import stepReducer, { epics as stepEpics } from '../views/step/redux';
|
||||||
import createQuizReducer from '../views/quiz/redux';
|
import quizReducer from '../views/quiz/redux';
|
||||||
import createProjectReducer from '../views/project/redux';
|
import projectReducer from '../views/project/redux';
|
||||||
|
|
||||||
// this is not great but is ok until we move to a different form type
|
// this is not great but is ok until we move to a different form type
|
||||||
export projectNormalizer from '../views/project/redux';
|
export projectNormalizer from '../views/project/redux';
|
||||||
@ -42,21 +44,20 @@ export const epics = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const types = createTypes([
|
export const types = createTypes([
|
||||||
|
'onRouteChallengeRoot',
|
||||||
|
'onRouteChallenges',
|
||||||
|
'onRouteCurrentChallenge',
|
||||||
// challenges
|
// challenges
|
||||||
// |- classic
|
// |- classic
|
||||||
'classicEditorUpdated',
|
'classicEditorUpdated',
|
||||||
'challengeUpdated',
|
'challengeUpdated',
|
||||||
'resetChallenge',
|
'clickOnReset',
|
||||||
'updateHint',
|
'updateHint',
|
||||||
'lockUntrustedCode',
|
'lockUntrustedCode',
|
||||||
'unlockUntrustedCode',
|
'unlockUntrustedCode',
|
||||||
'closeChallengeModal',
|
'closeChallengeModal',
|
||||||
'updateSuccessMessage',
|
'updateSuccessMessage',
|
||||||
|
|
||||||
// files
|
|
||||||
'updateFile',
|
|
||||||
'updateFiles',
|
|
||||||
|
|
||||||
// rechallenge
|
// rechallenge
|
||||||
'executeChallenge',
|
'executeChallenge',
|
||||||
'updateMain',
|
'updateMain',
|
||||||
@ -67,15 +68,9 @@ export const types = createTypes([
|
|||||||
'initOutput',
|
'initOutput',
|
||||||
'updateTests',
|
'updateTests',
|
||||||
'checkChallenge',
|
'checkChallenge',
|
||||||
'submitChallenge',
|
createAsyncTypes('submitChallenge'),
|
||||||
'moveToNextChallenge',
|
'moveToNextChallenge',
|
||||||
|
|
||||||
// code storage
|
|
||||||
'saveCode',
|
|
||||||
'loadCode',
|
|
||||||
'savedCodeFound',
|
|
||||||
'clearSavedCode',
|
|
||||||
|
|
||||||
// bug
|
// bug
|
||||||
'openBugModal',
|
'openBugModal',
|
||||||
'closeBugModal',
|
'closeBugModal',
|
||||||
@ -91,6 +86,11 @@ export const types = createTypes([
|
|||||||
'toggleStep'
|
'toggleStep'
|
||||||
], ns);
|
], ns);
|
||||||
|
|
||||||
|
// routes
|
||||||
|
export const onRouteChallenges = createAction(types.onRouteChallenges);
|
||||||
|
export const onRouteCurrentChallenge =
|
||||||
|
createAction(types.onRouteCurrentChallenge);
|
||||||
|
|
||||||
// classic
|
// classic
|
||||||
export const classicEditorUpdated = createAction(types.classicEditorUpdated);
|
export const classicEditorUpdated = createAction(types.classicEditorUpdated);
|
||||||
// challenges
|
// challenges
|
||||||
@ -106,10 +106,7 @@ export const challengeUpdated = createAction(
|
|||||||
types.challengeUpdated,
|
types.challengeUpdated,
|
||||||
challenge => ({ challenge })
|
challenge => ({ challenge })
|
||||||
);
|
);
|
||||||
export const resetChallenge = createAction(types.resetChallenge);
|
export const clickOnReset = createAction(types.clickOnReset);
|
||||||
// files
|
|
||||||
export const updateFile = createAction(types.updateFile);
|
|
||||||
export const updateFiles = createAction(types.updateFiles);
|
|
||||||
|
|
||||||
// rechallenge
|
// rechallenge
|
||||||
export const executeChallenge = createAction(
|
export const executeChallenge = createAction(
|
||||||
@ -130,16 +127,12 @@ export const updateOutput = createAction(types.updateOutput, loggerToStr);
|
|||||||
export const checkChallenge = createAction(types.checkChallenge);
|
export const checkChallenge = createAction(types.checkChallenge);
|
||||||
|
|
||||||
export const submitChallenge = createAction(types.submitChallenge);
|
export const submitChallenge = createAction(types.submitChallenge);
|
||||||
export const moveToNextChallenge = createAction(types.moveToNextChallenge);
|
export const submitChallengeComplete = createAction(
|
||||||
|
types.submitChallenge.complete,
|
||||||
// code storage
|
(username, points, challengeInfo) => ({ username, points, challengeInfo })
|
||||||
export const saveCode = createAction(types.saveCode);
|
|
||||||
export const loadCode = createAction(types.loadCode);
|
|
||||||
export const savedCodeFound = createAction(
|
|
||||||
types.savedCodeFound,
|
|
||||||
(files, challenge) => ({ files, challenge })
|
|
||||||
);
|
);
|
||||||
export const clearSavedCode = createAction(types.clearSavedCode);
|
|
||||||
|
export const moveToNextChallenge = createAction(types.moveToNextChallenge);
|
||||||
|
|
||||||
// bug
|
// bug
|
||||||
export const openBugModal = createAction(types.openBugModal);
|
export const openBugModal = createAction(types.openBugModal);
|
||||||
@ -151,9 +144,7 @@ const initialUiState = {
|
|||||||
output: null,
|
output: null,
|
||||||
isChallengeModalOpen: false,
|
isChallengeModalOpen: false,
|
||||||
isBugOpen: false,
|
isBugOpen: false,
|
||||||
successMessage: 'Happy Coding!',
|
successMessage: 'Happy Coding!'
|
||||||
hintIndex: 0,
|
|
||||||
numOfHints: 0
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
@ -163,7 +154,6 @@ const initialState = {
|
|||||||
helpChatRoom: 'Help',
|
helpChatRoom: 'Help',
|
||||||
// old code storage key
|
// old code storage key
|
||||||
legacyKey: '',
|
legacyKey: '',
|
||||||
files: {},
|
|
||||||
// map
|
// map
|
||||||
superBlocks: [],
|
superBlocks: [],
|
||||||
// misc
|
// misc
|
||||||
@ -172,7 +162,6 @@ const initialState = {
|
|||||||
|
|
||||||
export const getNS = state => state[ns];
|
export const getNS = state => state[ns];
|
||||||
export const keySelector = state => getNS(state).key;
|
export const keySelector = state => getNS(state).key;
|
||||||
export const filesSelector = state => getNS(state).files;
|
|
||||||
export const testsSelector = state => getNS(state).tests;
|
export const testsSelector = state => getNS(state).tests;
|
||||||
|
|
||||||
export const outputSelector = state => getNS(state).output;
|
export const outputSelector = state => getNS(state).output;
|
||||||
@ -186,7 +175,8 @@ export const challengeModalSelector =
|
|||||||
export const bugModalSelector = state => getNS(state).isBugOpen;
|
export const bugModalSelector = state => getNS(state).isBugOpen;
|
||||||
|
|
||||||
export const challengeMetaSelector = createSelector(
|
export const challengeMetaSelector = createSelector(
|
||||||
challengeSelector,
|
// use closure to get around circular deps
|
||||||
|
(...args) => challengeSelector(...args),
|
||||||
challenge => {
|
challenge => {
|
||||||
if (!challenge.id) {
|
if (!challenge.id) {
|
||||||
return {};
|
return {};
|
||||||
@ -214,15 +204,15 @@ export const challengeMetaSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function createReducers() {
|
export default combineReducers(
|
||||||
const setChallengeType = combineActions(
|
handleActions(
|
||||||
|
() => ({
|
||||||
|
[
|
||||||
|
combineActions(
|
||||||
types.challengeUpdated,
|
types.challengeUpdated,
|
||||||
app.fetchChallenge.complete
|
app.fetchChallenge.complete
|
||||||
);
|
)
|
||||||
|
]: (state, { payload: { challenge } }) => {
|
||||||
const mainReducer = handleActions(
|
|
||||||
{
|
|
||||||
[setChallengeType]: (state, { payload: { challenge } }) => {
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...initialUiState,
|
...initialUiState,
|
||||||
@ -230,10 +220,7 @@ export default function createReducers() {
|
|||||||
challenge: challenge.dashedName,
|
challenge: challenge.dashedName,
|
||||||
key: getFileKey(challenge),
|
key: getFileKey(challenge),
|
||||||
tests: createTests(challenge),
|
tests: createTests(challenge),
|
||||||
helpChatRoom: challenge.helpRoom || 'Help',
|
helpChatRoom: challenge.helpRoom || 'Help'
|
||||||
numOfHints: Array.isArray(challenge.hints) ?
|
|
||||||
challenge.hints.length :
|
|
||||||
0
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[types.updateTests]: (state, { payload: tests }) => ({
|
[types.updateTests]: (state, { payload: tests }) => ({
|
||||||
@ -252,12 +239,6 @@ export default function createReducers() {
|
|||||||
...state,
|
...state,
|
||||||
successMessage: payload
|
successMessage: payload
|
||||||
}),
|
}),
|
||||||
[types.updateHint]: state => ({
|
|
||||||
...state,
|
|
||||||
hintIndex: state.hintIndex + 1 >= state.numOfHints ?
|
|
||||||
0 :
|
|
||||||
state.hintIndex + 1
|
|
||||||
}),
|
|
||||||
[types.lockUntrustedCode]: state => ({
|
[types.lockUntrustedCode]: state => ({
|
||||||
...state,
|
...state,
|
||||||
isCodeLocked: true
|
isCodeLocked: true
|
||||||
@ -283,86 +264,11 @@ export default function createReducers() {
|
|||||||
|
|
||||||
[types.openBugModal]: state => ({ ...state, isBugOpen: true }),
|
[types.openBugModal]: state => ({ ...state, isBugOpen: true }),
|
||||||
[types.closeBugModal]: state => ({ ...state, isBugOpen: false })
|
[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 }) => {
|
initialState,
|
||||||
return files
|
ns
|
||||||
.reduce((files, file) => {
|
),
|
||||||
files[file.key] = file;
|
stepReducer,
|
||||||
return files;
|
quizReducer,
|
||||||
}, { ...state });
|
projectReducer
|
||||||
},
|
|
||||||
[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()
|
|
||||||
];
|
|
||||||
}
|
|
@ -1,6 +1,4 @@
|
|||||||
import flow from 'lodash/flow';
|
|
||||||
import * as challengeTypes from '../../utils/challengeTypes';
|
import * as challengeTypes from '../../utils/challengeTypes';
|
||||||
import { decodeScriptTags } from '../../../utils/encode-decode';
|
|
||||||
|
|
||||||
// determine the component to view for each challenge
|
// determine the component to view for each challenge
|
||||||
export const viewTypes = {
|
export const viewTypes = {
|
||||||
@ -43,36 +41,6 @@ export const submitTypes = {
|
|||||||
// has html that should be rendered
|
// has html that should be rendered
|
||||||
export const descriptionRegex = /\<blockquote|\<ol|\<h4|\<table/;
|
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 = [] }) {
|
export function createTests({ tests = [] }) {
|
||||||
return tests
|
return tests
|
||||||
.map(test => {
|
.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 { createSelector } from 'reselect';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import {
|
import {
|
@ -3,15 +3,19 @@ import React from 'react';
|
|||||||
import BackEnd from './Back-End.jsx';
|
import BackEnd from './Back-End.jsx';
|
||||||
import { types } from '../../redux';
|
import { types } from '../../redux';
|
||||||
import Panes from '../../../../Panes';
|
import Panes from '../../../../Panes';
|
||||||
|
import { createPaneMap } from '../../../../Panes/redux';
|
||||||
import _Map from '../../../../Map';
|
import _Map from '../../../../Map';
|
||||||
import ChildContainer from '../../../../Child-Container.jsx';
|
import ChildContainer from '../../../../Child-Container.jsx';
|
||||||
|
|
||||||
const propTypes = {};
|
const propTypes = {};
|
||||||
|
|
||||||
export const panesMap = {
|
export const panesMap = createPaneMap(
|
||||||
|
'backend',
|
||||||
|
() => ({
|
||||||
[types.toggleMap]: 'Map',
|
[types.toggleMap]: 'Map',
|
||||||
[types.toggleMain]: 'Main'
|
[types.toggleMain]: 'Main'
|
||||||
};
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const nameToComponentDef = {
|
const nameToComponentDef = {
|
||||||
Map: {
|
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 { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
@ -12,10 +13,11 @@ import {
|
|||||||
executeChallenge,
|
executeChallenge,
|
||||||
classicEditorUpdated,
|
classicEditorUpdated,
|
||||||
challengeMetaSelector,
|
challengeMetaSelector,
|
||||||
filesSelector,
|
|
||||||
keySelector
|
keySelector
|
||||||
} from '../../redux';
|
} from '../../redux';
|
||||||
|
|
||||||
|
import { filesSelector } from '../../../../files';
|
||||||
|
|
||||||
const envProps = typeof window !== 'undefined' ? Object.keys(window) : [];
|
const envProps = typeof window !== 'undefined' ? Object.keys(window) : [];
|
||||||
const options = {
|
const options = {
|
||||||
lint: {
|
lint: {
|
@ -3,19 +3,26 @@ import React from 'react';
|
|||||||
import SidePanel from './Side-Panel.jsx';
|
import SidePanel from './Side-Panel.jsx';
|
||||||
import Editor from './Editor.jsx';
|
import Editor from './Editor.jsx';
|
||||||
import Preview from './Preview.jsx';
|
import Preview from './Preview.jsx';
|
||||||
import { types } from '../../redux';
|
import { types, challengeMetaSelector } from '../../redux';
|
||||||
import Panes from '../../../../Panes';
|
import Panes from '../../../../Panes';
|
||||||
|
import { createPaneMap } from '../../../../Panes/redux';
|
||||||
import _Map from '../../../../Map';
|
import _Map from '../../../../Map';
|
||||||
import ChildContainer from '../../../../Child-Container.jsx';
|
import ChildContainer from '../../../../Child-Container.jsx';
|
||||||
|
|
||||||
const propTypes = {};
|
const propTypes = {};
|
||||||
|
|
||||||
export const panesMap = {
|
export const panesMap = createPaneMap(
|
||||||
|
'classic',
|
||||||
|
() => ({
|
||||||
[types.toggleMap]: 'Map',
|
[types.toggleMap]: 'Map',
|
||||||
[types.toggleSidePanel]: 'Side Panel',
|
[types.toggleSidePanel]: 'Side Panel',
|
||||||
[types.toggleClassicEditor]: 'Editor',
|
[types.toggleClassicEditor]: 'Editor',
|
||||||
[types.togglePreview]: 'Preview'
|
[types.togglePreview]: {
|
||||||
};
|
name: 'Preview',
|
||||||
|
filter: state => !!challengeMetaSelector(state).showPreview
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const nameToComponent = {
|
const nameToComponent = {
|
||||||
Map: {
|
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 ReactDom from 'react-dom';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@ -46,10 +47,7 @@ const mapStateToProps = createSelector(
|
|||||||
codeLockedSelector,
|
codeLockedSelector,
|
||||||
chatRoomSelector,
|
chatRoomSelector,
|
||||||
(
|
(
|
||||||
{
|
{ description },
|
||||||
description,
|
|
||||||
hints = []
|
|
||||||
},
|
|
||||||
{ title },
|
{ title },
|
||||||
tests,
|
tests,
|
||||||
output,
|
output,
|
||||||
@ -61,7 +59,6 @@ const mapStateToProps = createSelector(
|
|||||||
description,
|
description,
|
||||||
tests,
|
tests,
|
||||||
output,
|
output,
|
||||||
hint: hints[hintIndex],
|
|
||||||
isCodeLocked,
|
isCodeLocked,
|
||||||
helpChatRoom
|
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 { Button, ButtonGroup, Tooltip, OverlayTrigger } from 'react-bootstrap';
|
||||||
import PureComponent from 'react-pure-render/component';
|
import PureComponent from 'react-pure-render/component';
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export default class ToolPanel extends PureComponent {
|
|||||||
this.props.makeToast({
|
this.props.makeToast({
|
||||||
message: 'This will restore your code editor to its original state.',
|
message: 'This will restore your code editor to its original state.',
|
||||||
action: 'clear my code',
|
action: 'clear my code',
|
||||||
actionCreator: 'resetChallenge',
|
actionCreator: 'clickOnReset',
|
||||||
timeout: 4000
|
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 { reduxForm } from 'redux-form';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
@ -1,4 +1,5 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PureComponent from 'react-pure-render/component';
|
import PureComponent from 'react-pure-render/component';
|
@ -1,16 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import ns from './ns.json';
|
||||||
import Main from './Project.jsx';
|
import Main from './Project.jsx';
|
||||||
import { types } from '../../redux';
|
import { types } from '../../redux';
|
||||||
import Panes from '../../../../Panes';
|
import Panes from '../../../../Panes';
|
||||||
|
import { createPaneMap } from '../../../../Panes/redux';
|
||||||
import _Map from '../../../../Map';
|
import _Map from '../../../../Map';
|
||||||
import ChildContainer from '../../../../Child-Container.jsx';
|
import ChildContainer from '../../../../Child-Container.jsx';
|
||||||
|
|
||||||
const propTypes = {};
|
const propTypes = {};
|
||||||
export const panesMap = {
|
export const panesMap = createPaneMap(
|
||||||
|
ns,
|
||||||
|
() => ({
|
||||||
[types.toggleMap]: 'Map',
|
[types.toggleMap]: 'Map',
|
||||||
[types.toggleMain]: 'Main'
|
[types.toggleMain]: 'Main'
|
||||||
};
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const nameToComponent = {
|
const nameToComponent = {
|
||||||
Map: {
|
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';
|
import ChallengeTitle from '../../Challenge-Title.jsx';
|
||||||
|
|
||||||
const propTypes = {
|
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 PureComponent from 'react-pure-render/component';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
@ -1,5 +1,8 @@
|
|||||||
import { createTypes } from 'redux-create-types';
|
import {
|
||||||
import { createAction, handleActions } from 'redux-actions';
|
createAction,
|
||||||
|
createTypes,
|
||||||
|
handleActions
|
||||||
|
} from 'berkeleys-redux-utils';
|
||||||
import ns from '../ns.json';
|
import ns from '../ns.json';
|
||||||
|
|
||||||
export const types = createTypes([
|
export const types = createTypes([
|
||||||
@ -14,14 +17,13 @@ const initialState = {
|
|||||||
};
|
};
|
||||||
export const submittingSelector = state => state[ns].isSubmitting;
|
export const submittingSelector = state => state[ns].isSubmitting;
|
||||||
|
|
||||||
export default function createReducer() {
|
export default handleActions(
|
||||||
const reducer = handleActions({
|
() => ({
|
||||||
[types.showProjectSubmit]: state => ({
|
[types.showProjectSubmit]: state => ({
|
||||||
...state,
|
...state,
|
||||||
isSubmitting: true
|
isSubmitting: true
|
||||||
})
|
})
|
||||||
}, initialState);
|
}),
|
||||||
|
initialState,
|
||||||
reducer.toString = () => ns;
|
ns
|
||||||
return [ reducer ];
|
);
|
||||||
}
|
|
@ -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 { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import classnames from 'classnames';
|
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 { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
@ -1,16 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import ns from './ns.json';
|
||||||
import Main from './Quiz.jsx';
|
import Main from './Quiz.jsx';
|
||||||
import { types } from '../../redux';
|
import { types } from '../../redux';
|
||||||
import Panes from '../../../../Panes';
|
import Panes from '../../../../Panes';
|
||||||
|
import { createPaneMap } from '../../../../Panes/redux';
|
||||||
import _Map from '../../../../Map';
|
import _Map from '../../../../Map';
|
||||||
import ChildContainer from '../../../../Child-Container.jsx';
|
import ChildContainer from '../../../../Child-Container.jsx';
|
||||||
|
|
||||||
const propTypes = {};
|
const propTypes = {};
|
||||||
export const panesMap = {
|
export const panesMap = createPaneMap(
|
||||||
|
ns,
|
||||||
|
() => ({
|
||||||
[types.toggleMap]: 'Map',
|
[types.toggleMap]: 'Map',
|
||||||
[types.toggleMain]: 'Main'
|
[types.toggleMain]: 'Main'
|
||||||
};
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const nameToComponent = {
|
const nameToComponent = {
|
||||||
Map: {
|
Map: {
|
@ -1,5 +1,8 @@
|
|||||||
import { createTypes } from 'redux-create-types';
|
import {
|
||||||
import { createAction, handleActions } from 'redux-actions';
|
createAction,
|
||||||
|
createTypes,
|
||||||
|
handleActions
|
||||||
|
} from 'berkeleys-redux-utils';
|
||||||
import noop from 'lodash/noop';
|
import noop from 'lodash/noop';
|
||||||
|
|
||||||
import ns from '../ns.json';
|
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 selectedChoiceSelector = state => getNS(state).selectedChoice;
|
||||||
export const correctSelector = state => getNS(state).correct;
|
export const correctSelector = state => getNS(state).correct;
|
||||||
|
|
||||||
export default function createReducers() {
|
export default handleActions(
|
||||||
const reducer = handleActions({
|
() => ({
|
||||||
[types.nextQuestion]: state => ({
|
[types.nextQuestion]: state => ({
|
||||||
...state,
|
...state,
|
||||||
currentIndex: state.currentIndex + 1
|
currentIndex: state.currentIndex + 1
|
||||||
@ -72,8 +75,7 @@ export default function createReducers() {
|
|||||||
...state,
|
...state,
|
||||||
selectedChoice: null
|
selectedChoice: null
|
||||||
})
|
})
|
||||||
}, initialState);
|
}),
|
||||||
|
initialState,
|
||||||
reducer.toString = () => ns;
|
ns
|
||||||
return [ reducer ];
|
);
|
||||||
}
|
|
@ -1,16 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import ns from './ns.json';
|
||||||
import Step from './Step.jsx';
|
import Step from './Step.jsx';
|
||||||
import { types } from '../../redux';
|
import { types } from '../../redux';
|
||||||
import Panes from '../../../../Panes';
|
import Panes from '../../../../Panes';
|
||||||
|
import { createPaneMap } from '../../../../Panes/redux';
|
||||||
import _Map from '../../../../Map';
|
import _Map from '../../../../Map';
|
||||||
import ChildContainer from '../../../../Child-Container.jsx';
|
import ChildContainer from '../../../../Child-Container.jsx';
|
||||||
|
|
||||||
const propTypes = {};
|
const propTypes = {};
|
||||||
export const panesMap = {
|
export const panesMap = createPaneMap(
|
||||||
|
ns,
|
||||||
|
() => ({
|
||||||
[types.toggleMap]: 'Map',
|
[types.toggleMap]: 'Map',
|
||||||
[types.toggleStep]: 'Step'
|
[types.toggleStep]: 'Step'
|
||||||
};
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const nameToComponent = {
|
const nameToComponent = {
|
||||||
Map: {
|
Map: {
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user