Merge pull request #7430 from FreeCodeCamp/feature/react-challenges
Feature(app): Reactify FCC!
This commit is contained in:
82
.eslintrc
82
.eslintrc
@ -6,14 +6,26 @@
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"mocha": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
"react",
|
||||
"import"
|
||||
],
|
||||
"settings": {
|
||||
"import/ignore": [
|
||||
"node_modules",
|
||||
"\\.json$"
|
||||
],
|
||||
"import/extensions": [
|
||||
".js",
|
||||
".jsx"
|
||||
]
|
||||
},
|
||||
"globals": {
|
||||
"Promise": true,
|
||||
"window": true,
|
||||
@ -54,15 +66,15 @@
|
||||
"complexity": 0,
|
||||
"consistent-return": 2,
|
||||
"curly": 2,
|
||||
"default-case": 1,
|
||||
"default-case": 2,
|
||||
"dot-notation": 0,
|
||||
"eqeqeq": 1,
|
||||
"guard-for-in": 1,
|
||||
"no-alert": 1,
|
||||
"eqeqeq": 2,
|
||||
"guard-for-in": 2,
|
||||
"no-alert": 2,
|
||||
"no-caller": 2,
|
||||
"no-div-regex": 2,
|
||||
"no-else-return": 0,
|
||||
"no-eq-null": 1,
|
||||
"no-eq-null": 2,
|
||||
"no-eval": 2,
|
||||
"no-extend-native": 2,
|
||||
"no-extra-bind": 2,
|
||||
@ -72,8 +84,8 @@
|
||||
"no-iterator": 2,
|
||||
"no-labels": 2,
|
||||
"no-lone-blocks": 2,
|
||||
"no-loop-func": 1,
|
||||
"no-multi-spaces": 1,
|
||||
"no-loop-func": 2,
|
||||
"no-multi-spaces": 2,
|
||||
"no-multi-str": 2,
|
||||
"no-native-reassign": 2,
|
||||
"no-new": 2,
|
||||
@ -83,15 +95,15 @@
|
||||
"no-octal-escape": 2,
|
||||
"no-process-env": 0,
|
||||
"no-proto": 2,
|
||||
"no-redeclare": 1,
|
||||
"no-redeclare": 2,
|
||||
"no-return-assign": 2,
|
||||
"no-script-url": 2,
|
||||
"no-self-compare": 2,
|
||||
"no-sequences": 2,
|
||||
"no-unused-expressions": 2,
|
||||
"no-void": 1,
|
||||
"no-void": 2,
|
||||
"no-warning-comments": [
|
||||
1,
|
||||
2,
|
||||
{
|
||||
"terms": [
|
||||
"fixme"
|
||||
@ -114,7 +126,7 @@
|
||||
"no-shadow-restricted-names": 2,
|
||||
"no-undef": 2,
|
||||
"no-undef-init": 2,
|
||||
"no-undefined": 1,
|
||||
"no-undefined": 2,
|
||||
"no-unused-vars": 2,
|
||||
"no-use-before-define": 0,
|
||||
|
||||
@ -131,7 +143,7 @@
|
||||
"1tbs",
|
||||
{ "allowSingleLine": true }
|
||||
],
|
||||
"camelcase": 1,
|
||||
"camelcase": 2,
|
||||
"comma-spacing": [
|
||||
2,
|
||||
{
|
||||
@ -157,11 +169,11 @@
|
||||
"new-cap": 0,
|
||||
"new-parens": 2,
|
||||
"no-array-constructor": 2,
|
||||
"no-inline-comments": 1,
|
||||
"no-lonely-if": 1,
|
||||
"no-inline-comments": 2,
|
||||
"no-lonely-if": 2,
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-multiple-empty-lines": [
|
||||
1,
|
||||
2,
|
||||
{ "max": 2 }
|
||||
],
|
||||
"no-nested-ternary": 2,
|
||||
@ -169,7 +181,7 @@
|
||||
"semi-spacing": [2, { "before": false, "after": true }],
|
||||
"no-spaced-func": 2,
|
||||
"no-ternary": 0,
|
||||
"no-trailing-spaces": 1,
|
||||
"no-trailing-spaces": 2,
|
||||
"no-underscore-dangle": 0,
|
||||
"one-var": 0,
|
||||
"operator-assignment": 0,
|
||||
@ -198,7 +210,7 @@
|
||||
"space-in-parens": 0,
|
||||
"space-infix-ops": 2,
|
||||
"space-unary-ops": [
|
||||
1,
|
||||
2,
|
||||
{
|
||||
"words": true,
|
||||
"nonwords": false
|
||||
@ -209,7 +221,7 @@
|
||||
"always",
|
||||
{ "exceptions": ["-"] }
|
||||
],
|
||||
"wrap-regex": 1,
|
||||
"wrap-regex": 2,
|
||||
|
||||
"max-depth": 0,
|
||||
"max-len": [
|
||||
@ -219,22 +231,32 @@
|
||||
],
|
||||
"max-params": 0,
|
||||
"max-statements": 0,
|
||||
"no-bitwise": 1,
|
||||
"no-bitwise": 2,
|
||||
"no-plusplus": 0,
|
||||
|
||||
"react/display-name": 1,
|
||||
"react/jsx-boolean-value": [1, "always"],
|
||||
"jsx-quotes": [1, "prefer-single"],
|
||||
"react/jsx-no-undef": 1,
|
||||
"react/jsx-sort-props": [1, { "ignoreCase": true }],
|
||||
"react/jsx-uses-react": 1,
|
||||
"react/jsx-uses-vars": 1,
|
||||
"jsx-quotes": [2, "prefer-single"],
|
||||
"react/display-name": 2,
|
||||
"react/jsx-boolean-value": [2, "always"],
|
||||
"react/jsx-no-undef": 2,
|
||||
"react/jsx-sort-props": [2, { "ignoreCase": true }],
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
"react/no-did-mount-set-state": 2,
|
||||
"react/no-did-update-set-state": 2,
|
||||
"react/no-multi-comp": [2, { "ignoreStateless": true } ],
|
||||
"react/prop-types": 2,
|
||||
"react/react-in-jsx-scope": 1,
|
||||
"react/self-closing-comp": 1,
|
||||
"react/wrap-multilines": 1
|
||||
"react/react-in-jsx-scope": 2,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/wrap-multilines": 2,
|
||||
"react/jsx-closing-bracket-location": [ 2, { "selfClosing": "line-aligned", "nonEmpty": "props-aligned" } ],
|
||||
|
||||
"import/no-unresolved": 2,
|
||||
"import/named": 2,
|
||||
"import/namespace": 2,
|
||||
"import/default": 2,
|
||||
"import/export": 2,
|
||||
"import/imports-first": 2,
|
||||
"import/no-duplicates": 2,
|
||||
"import/newline-after-import": 2
|
||||
}
|
||||
}
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -29,6 +29,7 @@ main.css
|
||||
bundle.js
|
||||
coverage
|
||||
.remote-sync.json
|
||||
.tern-project
|
||||
|
||||
server/*.bundle.js
|
||||
public/js/bundle*
|
||||
@ -44,6 +45,7 @@ public/js/main*
|
||||
public/js/commonFramework*
|
||||
public/js/sandbox*
|
||||
public/js/iFrameScripts*
|
||||
public/js/frame-runner*
|
||||
public/js/plugin*
|
||||
public/js/vendor*
|
||||
public/js/faux*
|
||||
|
16
client/cold-reload.js
Normal file
16
client/cold-reload.js
Normal file
@ -0,0 +1,16 @@
|
||||
import store from 'store';
|
||||
|
||||
const key = '__cold-storage__';
|
||||
export function isColdStored() {
|
||||
return store.has(key);
|
||||
}
|
||||
|
||||
export function getColdStorage() {
|
||||
const coldReloadData = store.get(key);
|
||||
store.remove(key);
|
||||
return coldReloadData;
|
||||
}
|
||||
|
||||
export function saveToColdStorage(data) {
|
||||
store.set(key, data);
|
||||
}
|
73
client/frame-runner.js
Normal file
73
client/frame-runner.js
Normal file
@ -0,0 +1,73 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var common = parent.__common;
|
||||
var frameId = window.__frameId;
|
||||
var frameReady = common[frameId + 'Ready$'] || { onNext() {} };
|
||||
var Rx = document.Rx;
|
||||
var chai = parent.chai;
|
||||
var source = document.__source;
|
||||
|
||||
document.__getJsOutput = function getJsOutput() {
|
||||
if (window.__err || !common.shouldRun()) {
|
||||
return window.__err || 'source disabled';
|
||||
}
|
||||
let output;
|
||||
try {
|
||||
/* eslint-disable no-eval */
|
||||
output = eval(source);
|
||||
/* eslint-enable no-eval */
|
||||
} catch (e) {
|
||||
output = e.message + '\n' + e.stack;
|
||||
window.__err = e;
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
document.__runTests$ = function runTests$(tests = []) {
|
||||
/* eslint-disable no-unused-vars */
|
||||
const editor = { getValue() { return source; } };
|
||||
const code = source;
|
||||
/* eslint-enable no-unused-vars */
|
||||
if (window.__err) {
|
||||
return Rx.Observable.throw(window.__err);
|
||||
}
|
||||
|
||||
// Iterate through the test one at a time
|
||||
// on new stacks
|
||||
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
|
||||
// add delay here for firefox to catch up
|
||||
.delay(200)
|
||||
/* eslint-disable no-unused-vars */
|
||||
.map(({ text, testString }) => {
|
||||
const assert = chai.assert;
|
||||
/* eslint-enable no-unused-vars */
|
||||
const newTest = { text, testString };
|
||||
let test;
|
||||
try {
|
||||
/* eslint-disable no-eval */
|
||||
test = eval(testString);
|
||||
/* eslint-enable no-eval */
|
||||
if (typeof test === 'function') {
|
||||
// maybe sync/promise/observable
|
||||
if (test.length === 0) {
|
||||
test();
|
||||
}
|
||||
// callback test
|
||||
if (test.length === 1) {
|
||||
console.log('callback test');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
newTest.err = e.message + '\n' + e.stack;
|
||||
}
|
||||
if (!newTest.err) {
|
||||
newTest.pass = true;
|
||||
}
|
||||
return newTest;
|
||||
})
|
||||
// gather tests back into an array
|
||||
.toArray();
|
||||
};
|
||||
|
||||
// notify that the window methods are ready to run
|
||||
frameReady.onNext(null);
|
||||
});
|
@ -1,85 +0,0 @@
|
||||
/* eslint-disable no-undef, no-unused-vars, no-native-reassign */
|
||||
// the $ on the iframe window object is the same
|
||||
// as the one used on the main site, but
|
||||
// uses the iframe document as the context
|
||||
window.$(document).ready(function() {
|
||||
var _ = parent._;
|
||||
var Rx = parent.Rx;
|
||||
var chai = parent.chai;
|
||||
var assert = chai.assert;
|
||||
var tests = parent.tests;
|
||||
var common = parent.common;
|
||||
|
||||
common.getJsOutput = function evalJs(code = '') {
|
||||
if (window.__err || !common.shouldRun()) {
|
||||
return window.__err || 'code disabled';
|
||||
}
|
||||
let output;
|
||||
try {
|
||||
/* eslint-disable no-eval */
|
||||
output = eval(code);
|
||||
/* eslint-enable no-eval */
|
||||
} catch (e) {
|
||||
window.__err = e;
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
common.runPreviewTests$ =
|
||||
function runPreviewTests$({
|
||||
tests = [],
|
||||
originalCode,
|
||||
...rest
|
||||
}) {
|
||||
const code = originalCode;
|
||||
const editor = { getValue() { return originalCode; } };
|
||||
if (window.__err) {
|
||||
return Rx.Observable.throw(window.__err);
|
||||
}
|
||||
|
||||
// Iterate throught the test one at a time
|
||||
// on new stacks
|
||||
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
|
||||
// add delay here for firefox to catch up
|
||||
.delay(100)
|
||||
.map(test => {
|
||||
const userTest = {};
|
||||
try {
|
||||
/* eslint-disable no-eval */
|
||||
eval(test);
|
||||
/* eslint-enable no-eval */
|
||||
} catch (e) {
|
||||
userTest.err = e.message.split(':').shift();
|
||||
} finally {
|
||||
if (!test.match(/message: /g)) {
|
||||
// assumes test does not contain arrays
|
||||
// This is a patch until all test fall into this pattern
|
||||
userTest.text = test
|
||||
.split(',')
|
||||
.pop();
|
||||
userTest.text = 'message: ' + userTest.text + '\');';
|
||||
} else {
|
||||
userTest.text = test;
|
||||
}
|
||||
}
|
||||
return userTest;
|
||||
})
|
||||
// gather tests back into an array
|
||||
.toArray()
|
||||
.map(tests => ({ ...rest, tests, originalCode }));
|
||||
};
|
||||
|
||||
// used when updating preview without running tests
|
||||
common.checkPreview$ = function checkPreview$(args) {
|
||||
if (window.__err) {
|
||||
return Rx.Observable.throw(window.__err);
|
||||
}
|
||||
return Rx.Observable.just(args);
|
||||
};
|
||||
|
||||
// now that the runPreviewTest$ is defined
|
||||
// we set the subject to true
|
||||
// this will let the updatePreview
|
||||
// script now that we are ready.
|
||||
common.previewReady$.onNext(true);
|
||||
});
|
@ -3,69 +3,79 @@ import Rx from 'rx';
|
||||
import React from 'react';
|
||||
import debug from 'debug';
|
||||
import { Router } from 'react-router';
|
||||
import { routeReducer as routing, syncHistory } from 'react-router-redux';
|
||||
import {
|
||||
routerMiddleware,
|
||||
routerReducer as routing,
|
||||
syncHistoryWithStore
|
||||
} from 'react-router-redux';
|
||||
import { render } from 'redux-epic';
|
||||
import { createHistory } from 'history';
|
||||
import useLangRoutes from './utils/use-lang-routes';
|
||||
import sendPageAnalytics from './utils/send-page-analytics.js';
|
||||
|
||||
import app$ from '../common/app';
|
||||
import createApp from '../common/app';
|
||||
import provideStore from '../common/app/provide-store';
|
||||
|
||||
// client specific sagas
|
||||
import sagas from './sagas';
|
||||
|
||||
// render to observable
|
||||
import render from '../common/app/utils/render';
|
||||
import {
|
||||
isColdStored,
|
||||
getColdStorage,
|
||||
saveToColdStorage
|
||||
} from './cold-reload';
|
||||
|
||||
const isDev = Rx.config.longStackSupport = debug.enabled('fcc:*');
|
||||
const log = debug('fcc:client');
|
||||
const DOMContainer = document.getElementById('fcc');
|
||||
const initialState = window.__fcc__.data;
|
||||
const hotReloadTimeout = 5000;
|
||||
const csrfToken = window.__fcc__.csrf.token;
|
||||
const DOMContainer = document.getElementById('fcc');
|
||||
const initialState = isColdStored() ?
|
||||
getColdStorage() :
|
||||
window.__fcc__.data;
|
||||
initialState.app.csrfToken = csrfToken;
|
||||
|
||||
const serviceOptions = { xhrPath: '/services', context: { _csrf: csrfToken } };
|
||||
|
||||
Rx.config.longStackSupport = !!debug.enabled;
|
||||
const history = createHistory();
|
||||
const appLocation = history.createLocation(
|
||||
location.pathname + location.search
|
||||
);
|
||||
const routingMiddleware = syncHistory(history);
|
||||
const history = useLangRoutes(createHistory)();
|
||||
sendPageAnalytics(history, window.ga);
|
||||
|
||||
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
|
||||
const shouldRouterListenForReplays = !!window.devToolsExtension;
|
||||
const adjustUrlOnReplay = !!window.devToolsExtension;
|
||||
|
||||
const clientSagaOptions = { doc: document };
|
||||
const sagaOptions = {
|
||||
isDev,
|
||||
window,
|
||||
document: window.document,
|
||||
location: window.location,
|
||||
history: window.history
|
||||
};
|
||||
|
||||
// returns an observable
|
||||
app$({
|
||||
location: appLocation,
|
||||
history,
|
||||
serviceOptions,
|
||||
initialState,
|
||||
middlewares: [
|
||||
routingMiddleware,
|
||||
...sagas.map(saga => saga(clientSagaOptions))
|
||||
],
|
||||
reducers: { routing },
|
||||
enhancers: [ devTools ]
|
||||
})
|
||||
.flatMap(({ props, store }) => {
|
||||
|
||||
// because of weirdness in react-routers match function
|
||||
// we replace the wrapped returned in props with the first one
|
||||
// we passed in. This might be fixed in react-router 2.0
|
||||
props.history = history;
|
||||
|
||||
if (shouldRouterListenForReplays && store) {
|
||||
log('routing middleware listening for replays');
|
||||
routingMiddleware.listenForReplays(store);
|
||||
}
|
||||
|
||||
log('rendering');
|
||||
return render(
|
||||
provideStore(React.createElement(Router, props), store),
|
||||
DOMContainer
|
||||
);
|
||||
createApp({
|
||||
history,
|
||||
syncHistoryWithStore,
|
||||
syncOptions: { adjustUrlOnReplay },
|
||||
serviceOptions,
|
||||
initialState,
|
||||
middlewares: [ routerMiddleware(history) ],
|
||||
sagas: [...sagas ],
|
||||
sagaOptions,
|
||||
reducers: { routing },
|
||||
enhancers: [ devTools ]
|
||||
})
|
||||
.doOnNext(({ store }) => {
|
||||
if (module.hot && typeof module.hot.accept === 'function') {
|
||||
module.hot.accept('../common/app', function() {
|
||||
saveToColdStorage(store.getState());
|
||||
setTimeout(() => window.location.reload(), hotReloadTimeout);
|
||||
});
|
||||
}
|
||||
})
|
||||
.doOnNext(() => log('rendering'))
|
||||
.flatMap(({ props, store }) => render(
|
||||
provideStore(React.createElement(Router, props), store),
|
||||
DOMContainer
|
||||
))
|
||||
.subscribe(
|
||||
() => debug('react rendered'),
|
||||
err => { throw err; },
|
||||
|
@ -1,3 +1,8 @@
|
||||
.challenges-editor {
|
||||
height: 100%;
|
||||
width: 99%;
|
||||
}
|
||||
|
||||
.challenge-step-description {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
@ -6,6 +11,50 @@
|
||||
line-height: 44px;
|
||||
}
|
||||
|
||||
.challenge-step-forward-leave {
|
||||
transition: opacity .4s ease-in, transform .3s ease-in-out;
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.challenge-step-forward-leave-active {
|
||||
opacity: 0;
|
||||
transform: translate(-100%, 0);
|
||||
}
|
||||
|
||||
.challenge-step-forward-enter {
|
||||
transition: opacity .4s ease-in, transform .3s ease-in-out;
|
||||
opacity: 0;
|
||||
transform: translate(100%, 0);
|
||||
}
|
||||
|
||||
.challenge-step-forward-enter-active {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.challenge-step-backward-leave {
|
||||
transition: opacity .4s ease-in, transform .3s ease-in-out;
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.challenge-step-backward-leave-active {
|
||||
opacity: 0;
|
||||
transform: translate(100%, 0);
|
||||
}
|
||||
|
||||
.challenge-step-backward-enter {
|
||||
transition: opacity .4s ease-in, transform .3s ease-in-out;
|
||||
opacity: 0;
|
||||
transform: translate(-100%, 0);
|
||||
}
|
||||
|
||||
.challenge-step-backward-enter-active {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.challenge-instructions-title {
|
||||
margin-top: 0;
|
||||
i {
|
||||
@ -51,9 +100,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
#testSuite {
|
||||
.challenge-test-suite {
|
||||
margin-top: 10px;
|
||||
> div >.row {
|
||||
& .row {
|
||||
margin: 0!important;
|
||||
}
|
||||
}
|
||||
@ -69,17 +118,23 @@
|
||||
color: @gray-light;
|
||||
}
|
||||
|
||||
.big-error-icon {
|
||||
.big-icon {
|
||||
font-size: 30px;
|
||||
color: @brand-danger;
|
||||
top:50%;
|
||||
}
|
||||
|
||||
.big-success-icon {
|
||||
font-size: 30px;
|
||||
.error-icon {
|
||||
color: @brand-danger;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
color: @brand-primary;
|
||||
}
|
||||
|
||||
.refresh-icon {
|
||||
color: @alert-info-bg;
|
||||
}
|
||||
|
||||
iframe.iphone {
|
||||
border: none;
|
||||
@media(min-width: 992px) {
|
||||
@ -104,7 +159,7 @@ iframe.iphone {
|
||||
// To adjust right margin, negative values bring the image closer to the edge of the screen
|
||||
.iphone-position {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
top: -45px;
|
||||
z-index: -1;
|
||||
right: -195px;
|
||||
@media (min-width: 1200px) and (max-width: 1250px){
|
||||
@ -118,7 +173,7 @@ iframe.iphone {
|
||||
border-color: #515151;
|
||||
color: #ABABAB
|
||||
}
|
||||
div.CodeMirror {
|
||||
.CodeMirror {
|
||||
background-color:#242424;
|
||||
color:#ABABAB;
|
||||
&-gutters {
|
||||
|
@ -1,3 +1,4 @@
|
||||
.chat-embed-help-title,
|
||||
.chat-embed-main-title {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
27
client/less/code-mirror.less
Normal file
27
client/less/code-mirror.less
Normal file
@ -0,0 +1,27 @@
|
||||
.CodeMirror span {
|
||||
font-size: 18px;
|
||||
font-family: "Ubuntu Mono";
|
||||
padding-bottom: 0px;
|
||||
margin-bottom: 0px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border-radius: 5px;
|
||||
height: auto;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
font-size: 18px;
|
||||
font-family: "Ubuntu Mono";
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.challenge-log .CodeMirror {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
112
client/less/drawers.less
Normal file
112
client/less/drawers.less
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* based off of https://github.com/gitterHQ/sidecar
|
||||
* license: MIT
|
||||
*/
|
||||
.drawer {
|
||||
width:500px;
|
||||
|
||||
z-index: 20000;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
background-color: @body-bg;
|
||||
border-left: 1px solid #ddd;
|
||||
box-shadow: -12px 0 18px 0 rgba(50, 50, 50, 0.1);
|
||||
|
||||
transition: transform 0.3s cubic-bezier(0.16, 0.22, 0.22, 1.7);
|
||||
|
||||
&.is-collapsed:not(.is-loading) {
|
||||
-webkit-transform: translateX(110%);
|
||||
transform: translateX(110%);
|
||||
}
|
||||
|
||||
/* Add some "extension" so that there isn't a gap
|
||||
* when we translate(via animation) more than 100% */
|
||||
&:after {
|
||||
content: '';
|
||||
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
bottom: 0;
|
||||
right: -100%;
|
||||
|
||||
background-color: @body-bg;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.drawer-action-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
padding-bottom: 5px;
|
||||
padding-right:10px;
|
||||
padding-top:5px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.drawer-action-item {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
/* main axis */
|
||||
justify-content: center;
|
||||
/* cross axis */
|
||||
align-items: center;
|
||||
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
opacity: 0.65;
|
||||
background: none;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 22px 22px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
filter: hue-rotate(80deg) saturate(150);
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-action-pop-out {
|
||||
margin-right: -4px;
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDAgMTcxLjQyOSIgZmlsbD0iIzNhMzEzMyI+PHBhdGggZD0iTTE1Ny4xNDMsMTAzLjU3MXYzNS43MTRjMCw4Ljg1NC0zLjE0NCwxNi40MjYtOS40MzEsMjIuNzEzcy0xMy44NTgsOS40MzEtMjIuNzEyLDkuNDMxSDMyLjE0MyBjLTguODU0LDAtMTYuNDI1LTMuMTQ0LTIyLjcxMi05LjQzMVMwLDE0OC4xNCwwLDEzOS4yODVWNDYuNDI5YzAtOC44NTQsMy4xNDQtMTYuNDI1LDkuNDMxLTIyLjcxMiBjNi4yODctNi4yODcsMTMuODU4LTkuNDMxLDIyLjcxMi05LjQzMWg3OC41NzJjMS4wNDEsMCwxLjg5NiwwLjMzNSwyLjU2NiwxLjAwNGMwLjY3LDAuNjcsMS4wMDQsMS41MjUsMS4wMDQsMi41NjdWMjUgYzAsMS4wNDItMC4zMzQsMS44OTctMS4wMDQsMi41NjdjLTAuNjcsMC42Ny0xLjUyNSwxLjAwNC0yLjU2NiwxLjAwNEgzMi4xNDNjLTQuOTExLDAtOS4xMTUsMS43NDktMTIuNjEyLDUuMjQ2IHMtNS4yNDYsNy43MDEtNS4yNDYsMTIuNjEydjkyLjg1NmMwLDQuOTExLDEuNzQ5LDkuMTE1LDUuMjQ2LDEyLjYxMnM3LjcwMSw1LjI0NSwxMi42MTIsNS4yNDVIMTI1YzQuOTEsMCw5LjExNS0xLjc0OCwxMi42MTEtNS4yNDUgYzMuNDk3LTMuNDk3LDUuMjQ2LTcuNzAxLDUuMjQ2LTEyLjYxMnYtMzUuNzE0YzAtMS4wNDIsMC4zMzQtMS44OTcsMS4wMDQtMi41NjdjMC42Ny0wLjY2OSwxLjUyNS0xLjAwNCwyLjU2Ny0xLjAwNGg3LjE0MyBjMS4wNDIsMCwxLjg5NywwLjMzNSwyLjU2NywxLjAwNEMxNTYuODA5LDEwMS42NzQsMTU3LjE0MywxMDIuNTI5LDE1Ny4xNDMsMTAzLjU3MXogTTIwMCw3LjE0M3Y1Ny4xNDMgYzAsMS45MzUtMC43MDcsMy42MDktMi4xMjEsNS4wMjJjLTEuNDEzLDEuNDE0LTMuMDg4LDIuMTIxLTUuMDIxLDIuMTIxYy0xLjkzNSwwLTMuNjA5LTAuNzA3LTUuMDIyLTIuMTIxbC0xOS42NDQtMTkuNjQzIGwtNzIuNzY3LDcyLjc2OWMtMC43NDQsMC43NDQtMS42LDEuMTE1LTIuNTY3LDEuMTE1cy0xLjgyMy0wLjM3MS0yLjU2Ny0xLjExNUw3Ny41NjcsMTA5LjcxYy0wLjc0NC0wLjc0NC0xLjExNi0xLjYtMS4xMTYtMi41NjcgYzAtMC45NjcsMC4zNzItMS44MjIsMS4xMTYtMi41NjZsNzIuNzY4LTcyLjc2OGwtMTkuNjQ0LTE5LjY0M2MtMS40MTMtMS40MTQtMi4xMi0zLjA4OC0yLjEyLTUuMDIyYzAtMS45MzUsMC43MDctMy42MDksMi4xMi01LjAyMiBDMTMyLjEwNSwwLjcwNywxMzMuNzc5LDAsMTM1LjcxNSwwaDU3LjE0M2MxLjkzNCwwLDMuNjA4LDAuNzA3LDUuMDIxLDIuMTIxQzE5OS4yOTMsMy41MzQsMjAwLDUuMjA4LDIwMCw3LjE0M3oiLz48L3N2Zz4=)
|
||||
}
|
||||
|
||||
.drawer-action-collapse {
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNzEuNDI5IDE3MS40MjkiIGZpbGw9IiMzYTMxMzMiPjxwYXRoIGQ9Ik0xMjIuNDMzLDEwNi4xMzhsLTE2LjI5NSwxNi4yOTVjLTAuNzQ0LDAuNzQ0LTEuNiwxLjExNi0yLjU2NiwxLjExNmMtMC45NjgsMC0xLjgyMy0wLjM3Mi0yLjU2Ny0xLjExNmwtMTUuMjktMTUuMjkgbC0xNS4yOSwxNS4yOWMtMC43NDQsMC43NDQtMS42LDEuMTE2LTIuNTY3LDEuMTE2cy0xLjgyMy0wLjM3Mi0yLjU2Ny0xLjExNmwtMTYuMjk0LTE2LjI5NWMtMC43NDQtMC43NDQtMS4xMTYtMS42LTEuMTE2LTIuNTY2IGMwLTAuOTY4LDAuMzcyLTEuODIzLDEuMTE2LTIuNTY3bDE1LjI5LTE1LjI5bC0xNS4yOS0xNS4yOWMtMC43NDQtMC43NDQtMS4xMTYtMS42LTEuMTE2LTIuNTY3czAuMzcyLTEuODIzLDEuMTE2LTIuNTY3IEw2NS4yOSw0OC45OTZjMC43NDQtMC43NDQsMS42LTEuMTE2LDIuNTY3LTEuMTE2czEuODIzLDAuMzcyLDIuNTY3LDEuMTE2bDE1LjI5LDE1LjI5bDE1LjI5LTE1LjI5IGMwLjc0NC0wLjc0NCwxLjYtMS4xMTYsMi41NjctMS4xMTZjMC45NjcsMCwxLjgyMiwwLjM3MiwyLjU2NiwxLjExNmwxNi4yOTUsMTYuMjk0YzAuNzQ0LDAuNzQ0LDEuMTE2LDEuNiwxLjExNiwyLjU2NyBzLTAuMzcyLDEuODIzLTEuMTE2LDIuNTY3bC0xNS4yOSwxNS4yOWwxNS4yOSwxNS4yOWMwLjc0NCwwLjc0NCwxLjExNiwxLjYsMS4xMTYsMi41NjcgQzEyMy41NDksMTA0LjUzOSwxMjMuMTc3LDEwNS4zOTQsMTIyLjQzMywxMDYuMTM4eiBNMTQ2LjQyOSw4NS43MTRjMC0xMS4wMTItMi43MTctMjEuMTY4LTguMTQ4LTMwLjQ2OSBzLTEyLjc5Ny0xNi42NjctMjIuMDk4LTIyLjA5OFM5Ni43MjYsMjUsODUuNzE0LDI1cy0yMS4xNjgsMi43MTYtMzAuNDY5LDguMTQ3UzM4LjU3OSw0NS45NDUsMzMuMTQ3LDU1LjI0NlMyNSw3NC43MDMsMjUsODUuNzE0IHMyLjcxNiwyMS4xNjgsOC4xNDcsMzAuNDY5czEyLjc5NywxNi42NjYsMjIuMDk4LDIyLjA5OHMxOS40NTcsOC4xNDgsMzAuNDY5LDguMTQ4czIxLjE2OC0yLjcxNywzMC40NjktOC4xNDggczE2LjY2Ni0xMi43OTcsMjIuMDk4LTIyLjA5OFMxNDYuNDI5LDk2LjcyNiwxNDYuNDI5LDg1LjcxNHogTTE3MS40MjksODUuNzE0YzAsMTUuNTUxLTMuODMyLDI5Ljg5My0xMS40OTYsNDMuMDI0IGMtNy42NjQsMTMuMTMzLTE4LjA2MiwyMy41My0zMS4xOTQsMzEuMTk0Yy0xMy4xMzIsNy42NjQtMjcuNDc0LDExLjQ5Ni00My4wMjQsMTEuNDk2cy0yOS44OTItMy44MzItNDMuMDI0LTExLjQ5NiBjLTEzLjEzMy03LjY2NC0yMy41MzEtMTguMDYyLTMxLjE5NC0zMS4xOTRDMy44MzIsMTE1LjYwNywwLDEwMS4yNjUsMCw4NS43MTRTMy44MzIsNTUuODIyLDExLjQ5Niw0Mi42OSBjNy42NjQtMTMuMTMzLDE4LjA2Mi0yMy41MzEsMzEuMTk0LTMxLjE5NEM1NS44MjIsMy44MzIsNzAuMTY0LDAsODUuNzE0LDBzMjkuODkzLDMuODMyLDQzLjAyNCwxMS40OTYgYzEzLjEzMyw3LjY2NCwyMy41MywxOC4wNjIsMzEuMTk0LDMxLjE5NEMxNjcuNTk3LDU1LjgyMiwxNzEuNDI5LDcwLjE2NCwxNzEuNDI5LDg1LjcxNHoiLz48L3N2Zz4=)
|
||||
}
|
3338
client/less/lib/animate.less
vendored
Normal file
3338
client/less/lib/animate.less
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -63,7 +63,7 @@
|
||||
// Addon and addon wrapper for buttons
|
||||
.input-group-addon,
|
||||
.input-group-btn {
|
||||
width: 1%;
|
||||
min-width: 1%;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle; // Match the inputs
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import "lib/bootstrap/bootstrap";
|
||||
@import "lib/bootstrap-social/bootstrap-social";
|
||||
@import "lib/ionicons/ionicons";
|
||||
@import "lib/animate.min.less";
|
||||
@import "lib/animate";
|
||||
@import "lib/bootstrap/variables";
|
||||
|
||||
html,body,div,span,a,li,td,th {
|
||||
@ -67,6 +67,11 @@ body.no-top-and-bottom-margins {
|
||||
margin: 75px 20px 0px 20px;
|
||||
}
|
||||
|
||||
body.react-layout {
|
||||
margin-top: 75px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
font-weight: 400;
|
||||
}
|
||||
@ -682,62 +687,19 @@ form.update-email .btn{
|
||||
padding-bottom: 117%;
|
||||
}
|
||||
|
||||
#directions {
|
||||
text-align: left;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.graph-rect {
|
||||
fill: #ddd !important
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Challenge styling
|
||||
*/
|
||||
|
||||
form.code span {
|
||||
font-size: 18px;
|
||||
font-family: "Ubuntu Mono";
|
||||
padding-bottom: 0px;
|
||||
margin-bottom: 0px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
font-size: 18px;
|
||||
font-family: "Ubuntu Mono";
|
||||
}
|
||||
|
||||
#mainEditorPanel {
|
||||
height: 100%;
|
||||
width: 99%;
|
||||
}
|
||||
|
||||
.scroll-locker {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#mainEditorPanel .panel-body {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
div.CodeMirror-scroll {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.test-vertical-center {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.cm-s-monokai.CodeMirror {
|
||||
border-radius: 5px;
|
||||
}
|
||||
.courseware-height {
|
||||
min-height: 650px;
|
||||
}
|
||||
@ -1177,10 +1139,8 @@ and (max-width : 400px) {
|
||||
|
||||
@import "chat.less";
|
||||
@import "jobs.less";
|
||||
@import 'code-mirror.less';
|
||||
@import "challenge.less";
|
||||
@import "toastr.less";
|
||||
@import "map.less";
|
||||
|
||||
ul > li {
|
||||
list-style-type: disc;
|
||||
}
|
||||
@import "drawers.less";
|
||||
|
@ -2,116 +2,6 @@
|
||||
* based off of https://github.com/gitterHQ/sidecar
|
||||
* license: MIT
|
||||
*/
|
||||
|
||||
.map-aside {
|
||||
width:500px;
|
||||
|
||||
z-index: 20000;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
background-color: @body-bg;
|
||||
border-left: 1px solid #ddd;
|
||||
box-shadow: -12px 0 18px 0 rgba(50, 50, 50, 0.1);
|
||||
|
||||
transition: transform 0.3s cubic-bezier(0.16, 0.22, 0.22, 1.7);
|
||||
|
||||
&.is-collapsed:not(.is-loading) {
|
||||
-webkit-transform: translateX(110%);
|
||||
transform: translateX(110%);
|
||||
}
|
||||
|
||||
/* Add some "extension" so that there isn't a gap
|
||||
* when we translate(via animation) more than 100% */
|
||||
&:after {
|
||||
content: '';
|
||||
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
bottom: 0;
|
||||
right: -100%;
|
||||
|
||||
background-color: @body-bg;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.map-aside-action-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
padding-bottom: 5px;
|
||||
padding-right:10px;
|
||||
padding-top:5px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.map-fixed-header {
|
||||
position: fixed;
|
||||
background: white;
|
||||
padding-top: 5px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
@media (max-width: 720px) {
|
||||
padding-top:30px;
|
||||
}
|
||||
p {
|
||||
margin: 5px 0 20px;
|
||||
@media (max-width: 720px) {
|
||||
margin-bottom:10px;
|
||||
}
|
||||
@media only screen and (min-width: 480px) and (max-width: 670px) {
|
||||
margin-top: -30px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
@media only screen and (min-width: 200px) and (max-width: 479px) {
|
||||
margin-top: -30px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
margin:30px 0;
|
||||
|
||||
@media (max-width: 720px) {
|
||||
margin:25px 0;
|
||||
}
|
||||
@media only screen and (min-width: 480px) and (max-width: 670px) {
|
||||
margin: 15px 0;
|
||||
}
|
||||
@media only screen and (min-width: 200px) and (max-width: 479px) {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
.flashMessage {
|
||||
position:fixed;
|
||||
margin: 0 auto;
|
||||
z-index: 2;
|
||||
top: 160px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.map-buttons {
|
||||
margin-top: -10px;
|
||||
& button,
|
||||
@ -125,21 +15,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
#map-filter {
|
||||
.map-filter {
|
||||
background:#fff;
|
||||
border-color: darkgreen;
|
||||
}
|
||||
.input-group-addon {
|
||||
width:40px;
|
||||
|
||||
.map-filter + .input-group-addon {
|
||||
width: 40px;
|
||||
color: darkgreen;
|
||||
background: #fff;
|
||||
background-color: #fff;
|
||||
border-color: darkgreen;
|
||||
&.filled{
|
||||
background: darkgreen;
|
||||
border-color: #000d00;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.fa {
|
||||
position:absolute;
|
||||
top:50%;
|
||||
@ -149,22 +34,67 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mapWrapper {
|
||||
.map-filter.filled + span.input-group-addon {
|
||||
background: darkgreen;
|
||||
border-color: #000d00;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
span {
|
||||
display: inline-block;
|
||||
min-height: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.map-wrapper {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.drawer .map-fixed-header {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.map-accordion {
|
||||
width:700px;
|
||||
margin:155px auto 0;
|
||||
position:relative;
|
||||
#nested {
|
||||
margin:0 10px;
|
||||
@media (max-width: 400px) {
|
||||
margin:0;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
overflow-y: auto;
|
||||
|
||||
.map-accordion-panel-title {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.map-accordion-panel-collapse {
|
||||
transition: height 0.001s;
|
||||
}
|
||||
|
||||
.map-accordion-panel-nested-collapse {
|
||||
transition: height 0.001s;
|
||||
}
|
||||
|
||||
.map-accordion-panel-nested {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.map-accordion-panel-nested-body {
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.map-accordion-panel-nested {
|
||||
margin: 0;
|
||||
}
|
||||
.map-accordion-panel-nested-title {
|
||||
padding-left: 9px;
|
||||
}
|
||||
.map-accordion-panel-nested-body {
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
a:focus {
|
||||
text-decoration: none;
|
||||
color:darkgreen;
|
||||
@ -173,6 +103,7 @@
|
||||
text-decoration: underline;
|
||||
color:#001800;
|
||||
}
|
||||
|
||||
h2 > a {
|
||||
width:100%;
|
||||
display:block;
|
||||
@ -182,24 +113,26 @@
|
||||
padding-right:20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
a {
|
||||
margin:15px 0;
|
||||
padding:0;
|
||||
|
||||
&:first-child {
|
||||
margin-top:25px
|
||||
}
|
||||
> a {
|
||||
|
||||
> h3 {
|
||||
padding-left: 40px;
|
||||
padding-bottom: 10px;
|
||||
display:block;
|
||||
max-width: 535px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
div.chapterBlock {
|
||||
.map-accordion-block {
|
||||
:before {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
p {
|
||||
text-indent: -15px;
|
||||
margin-left: 60px;
|
||||
@ -210,43 +143,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.challengeBlockDescription {
|
||||
margin:0;
|
||||
margin-top:-10px;
|
||||
padding:0 15px 23px 30px;
|
||||
}
|
||||
|
||||
span.no-link-underline {
|
||||
position:absolute;
|
||||
margin-left:-30px;
|
||||
color: #666;
|
||||
}
|
||||
div > div:last-child {
|
||||
margin-bottom:30px
|
||||
}
|
||||
}
|
||||
.challengeBlockTime {
|
||||
font-size: 18px;
|
||||
color: #BBBBBB;
|
||||
display:block;
|
||||
margin-left: 40px;
|
||||
margin-bottom: 20px;
|
||||
@media (min-width: 721px) {
|
||||
margin-right: 20px;
|
||||
margin-top:-30px;
|
||||
float:right;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.map-accordion {
|
||||
left:0;
|
||||
right:0;
|
||||
width:100%;
|
||||
top:195px;
|
||||
bottom:0;
|
||||
margin:0;
|
||||
position:absolute;
|
||||
@media (max-width: 720px) {
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
top: 195px;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
// position:absolute;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@ -257,155 +161,60 @@
|
||||
margin-top:0;
|
||||
}
|
||||
> a {
|
||||
padding:10px 0;
|
||||
padding-left:50px;
|
||||
padding-right:20px;
|
||||
font-size:20px;
|
||||
padding: 10px 0;
|
||||
padding-left: 50px;
|
||||
padding-right: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
margin:10px 0;
|
||||
padding:0;
|
||||
> a {
|
||||
a {
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
> h3 {
|
||||
clear:both;
|
||||
font-size:20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.challenge-block-description {
|
||||
margin:0;
|
||||
margin-top:-10px;
|
||||
padding:0 15px 23px 30px;
|
||||
}
|
||||
|
||||
span.no-link-underline {
|
||||
margin-left:-30px;
|
||||
color: #666;
|
||||
}
|
||||
div > div:last-child {
|
||||
margin-bottom:30px
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 480px) and (max-width: 670px) {
|
||||
.map-fixed-header {
|
||||
.row.map-buttons {
|
||||
width: 50%;
|
||||
float: left;
|
||||
padding-bottom: 10px;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
button, button.map-buttons {
|
||||
width: 100%;
|
||||
}
|
||||
.input-group {
|
||||
margin-top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.map-buttons:first-of-type {
|
||||
float: right;
|
||||
padding-left: 10px;
|
||||
}
|
||||
hr {
|
||||
clear: both;
|
||||
margin: 9px 0;
|
||||
}
|
||||
.challenge-block-time {
|
||||
font-size: 18px;
|
||||
color: #BBBBBB;
|
||||
margin-bottom: 20px;
|
||||
@media (min-width: 721px) {
|
||||
// margin-right: 20px;
|
||||
// margin-top: -30px;
|
||||
float: right;
|
||||
}
|
||||
.map-accordion {
|
||||
top: 136px;
|
||||
h2 {
|
||||
margin: 5px 0;
|
||||
a {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
h3 > a {
|
||||
font-size: 15px;
|
||||
}
|
||||
h3:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 200px) and (max-width: 479px) {
|
||||
.map-fixed-header {
|
||||
.row.map-buttons {
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
button, button.map-buttons {
|
||||
width: 100%;
|
||||
}
|
||||
.input-group {
|
||||
margin-top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-accordion {
|
||||
top: 172px;
|
||||
h2 {
|
||||
margin: 10px 0;
|
||||
a {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
h3 > a {
|
||||
font-size: 17px;
|
||||
}
|
||||
h3:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-aside-action-item {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
/* main axis */
|
||||
justify-content: center;
|
||||
/* cross axis */
|
||||
align-items: center;
|
||||
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
opacity: 0.65;
|
||||
background: none;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 22px 22px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
filter: hue-rotate(80deg) saturate(150);
|
||||
}
|
||||
}
|
||||
|
||||
#noneFound {
|
||||
display:none;
|
||||
margin:60px 30px 0;
|
||||
font-size:30px;
|
||||
text-align: center;
|
||||
color:darkgreen;
|
||||
.fa {
|
||||
display:block;
|
||||
font-size:300px;
|
||||
}
|
||||
display:none;
|
||||
margin:60px 30px 0;
|
||||
font-size:30px;
|
||||
text-align: center;
|
||||
color:darkgreen;
|
||||
.fa {
|
||||
display:block;
|
||||
font-size:300px;
|
||||
}
|
||||
}
|
||||
|
||||
.map-aside-action-pop-out {
|
||||
margin-right: -4px;
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDAgMTcxLjQyOSIgZmlsbD0iIzNhMzEzMyI+PHBhdGggZD0iTTE1Ny4xNDMsMTAzLjU3MXYzNS43MTRjMCw4Ljg1NC0zLjE0NCwxNi40MjYtOS40MzEsMjIuNzEzcy0xMy44NTgsOS40MzEtMjIuNzEyLDkuNDMxSDMyLjE0MyBjLTguODU0LDAtMTYuNDI1LTMuMTQ0LTIyLjcxMi05LjQzMVMwLDE0OC4xNCwwLDEzOS4yODVWNDYuNDI5YzAtOC44NTQsMy4xNDQtMTYuNDI1LDkuNDMxLTIyLjcxMiBjNi4yODctNi4yODcsMTMuODU4LTkuNDMxLDIyLjcxMi05LjQzMWg3OC41NzJjMS4wNDEsMCwxLjg5NiwwLjMzNSwyLjU2NiwxLjAwNGMwLjY3LDAuNjcsMS4wMDQsMS41MjUsMS4wMDQsMi41NjdWMjUgYzAsMS4wNDItMC4zMzQsMS44OTctMS4wMDQsMi41NjdjLTAuNjcsMC42Ny0xLjUyNSwxLjAwNC0yLjU2NiwxLjAwNEgzMi4xNDNjLTQuOTExLDAtOS4xMTUsMS43NDktMTIuNjEyLDUuMjQ2IHMtNS4yNDYsNy43MDEtNS4yNDYsMTIuNjEydjkyLjg1NmMwLDQuOTExLDEuNzQ5LDkuMTE1LDUuMjQ2LDEyLjYxMnM3LjcwMSw1LjI0NSwxMi42MTIsNS4yNDVIMTI1YzQuOTEsMCw5LjExNS0xLjc0OCwxMi42MTEtNS4yNDUgYzMuNDk3LTMuNDk3LDUuMjQ2LTcuNzAxLDUuMjQ2LTEyLjYxMnYtMzUuNzE0YzAtMS4wNDIsMC4zMzQtMS44OTcsMS4wMDQtMi41NjdjMC42Ny0wLjY2OSwxLjUyNS0xLjAwNCwyLjU2Ny0xLjAwNGg3LjE0MyBjMS4wNDIsMCwxLjg5NywwLjMzNSwyLjU2NywxLjAwNEMxNTYuODA5LDEwMS42NzQsMTU3LjE0MywxMDIuNTI5LDE1Ny4xNDMsMTAzLjU3MXogTTIwMCw3LjE0M3Y1Ny4xNDMgYzAsMS45MzUtMC43MDcsMy42MDktMi4xMjEsNS4wMjJjLTEuNDEzLDEuNDE0LTMuMDg4LDIuMTIxLTUuMDIxLDIuMTIxYy0xLjkzNSwwLTMuNjA5LTAuNzA3LTUuMDIyLTIuMTIxbC0xOS42NDQtMTkuNjQzIGwtNzIuNzY3LDcyLjc2OWMtMC43NDQsMC43NDQtMS42LDEuMTE1LTIuNTY3LDEuMTE1cy0xLjgyMy0wLjM3MS0yLjU2Ny0xLjExNUw3Ny41NjcsMTA5LjcxYy0wLjc0NC0wLjc0NC0xLjExNi0xLjYtMS4xMTYtMi41NjcgYzAtMC45NjcsMC4zNzItMS44MjIsMS4xMTYtMi41NjZsNzIuNzY4LTcyLjc2OGwtMTkuNjQ0LTE5LjY0M2MtMS40MTMtMS40MTQtMi4xMi0zLjA4OC0yLjEyLTUuMDIyYzAtMS45MzUsMC43MDctMy42MDksMi4xMi01LjAyMiBDMTMyLjEwNSwwLjcwNywxMzMuNzc5LDAsMTM1LjcxNSwwaDU3LjE0M2MxLjkzNCwwLDMuNjA4LDAuNzA3LDUuMDIxLDIuMTIxQzE5OS4yOTMsMy41MzQsMjAwLDUuMjA4LDIwMCw3LjE0M3oiLz48L3N2Zz4=)
|
||||
}
|
||||
|
||||
.map-aside-action-collapse {
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNzEuNDI5IDE3MS40MjkiIGZpbGw9IiMzYTMxMzMiPjxwYXRoIGQ9Ik0xMjIuNDMzLDEwNi4xMzhsLTE2LjI5NSwxNi4yOTVjLTAuNzQ0LDAuNzQ0LTEuNiwxLjExNi0yLjU2NiwxLjExNmMtMC45NjgsMC0xLjgyMy0wLjM3Mi0yLjU2Ny0xLjExNmwtMTUuMjktMTUuMjkgbC0xNS4yOSwxNS4yOWMtMC43NDQsMC43NDQtMS42LDEuMTE2LTIuNTY3LDEuMTE2cy0xLjgyMy0wLjM3Mi0yLjU2Ny0xLjExNmwtMTYuMjk0LTE2LjI5NWMtMC43NDQtMC43NDQtMS4xMTYtMS42LTEuMTE2LTIuNTY2IGMwLTAuOTY4LDAuMzcyLTEuODIzLDEuMTE2LTIuNTY3bDE1LjI5LTE1LjI5bC0xNS4yOS0xNS4yOWMtMC43NDQtMC43NDQtMS4xMTYtMS42LTEuMTE2LTIuNTY3czAuMzcyLTEuODIzLDEuMTE2LTIuNTY3IEw2NS4yOSw0OC45OTZjMC43NDQtMC43NDQsMS42LTEuMTE2LDIuNTY3LTEuMTE2czEuODIzLDAuMzcyLDIuNTY3LDEuMTE2bDE1LjI5LDE1LjI5bDE1LjI5LTE1LjI5IGMwLjc0NC0wLjc0NCwxLjYtMS4xMTYsMi41NjctMS4xMTZjMC45NjcsMCwxLjgyMiwwLjM3MiwyLjU2NiwxLjExNmwxNi4yOTUsMTYuMjk0YzAuNzQ0LDAuNzQ0LDEuMTE2LDEuNiwxLjExNiwyLjU2NyBzLTAuMzcyLDEuODIzLTEuMTE2LDIuNTY3bC0xNS4yOSwxNS4yOWwxNS4yOSwxNS4yOWMwLjc0NCwwLjc0NCwxLjExNiwxLjYsMS4xMTYsMi41NjcgQzEyMy41NDksMTA0LjUzOSwxMjMuMTc3LDEwNS4zOTQsMTIyLjQzMywxMDYuMTM4eiBNMTQ2LjQyOSw4NS43MTRjMC0xMS4wMTItMi43MTctMjEuMTY4LTguMTQ4LTMwLjQ2OSBzLTEyLjc5Ny0xNi42NjctMjIuMDk4LTIyLjA5OFM5Ni43MjYsMjUsODUuNzE0LDI1cy0yMS4xNjgsMi43MTYtMzAuNDY5LDguMTQ3UzM4LjU3OSw0NS45NDUsMzMuMTQ3LDU1LjI0NlMyNSw3NC43MDMsMjUsODUuNzE0IHMyLjcxNiwyMS4xNjgsOC4xNDcsMzAuNDY5czEyLjc5NywxNi42NjYsMjIuMDk4LDIyLjA5OHMxOS40NTcsOC4xNDgsMzAuNDY5LDguMTQ4czIxLjE2OC0yLjcxNywzMC40NjktOC4xNDggczE2LjY2Ni0xMi43OTcsMjIuMDk4LTIyLjA5OFMxNDYuNDI5LDk2LjcyNiwxNDYuNDI5LDg1LjcxNHogTTE3MS40MjksODUuNzE0YzAsMTUuNTUxLTMuODMyLDI5Ljg5My0xMS40OTYsNDMuMDI0IGMtNy42NjQsMTMuMTMzLTE4LjA2MiwyMy41My0zMS4xOTQsMzEuMTk0Yy0xMy4xMzIsNy42NjQtMjcuNDc0LDExLjQ5Ni00My4wMjQsMTEuNDk2cy0yOS44OTItMy44MzItNDMuMDI0LTExLjQ5NiBjLTEzLjEzMy03LjY2NC0yMy41MzEtMTguMDYyLTMxLjE5NC0zMS4xOTRDMy44MzIsMTE1LjYwNywwLDEwMS4yNjUsMCw4NS43MTRTMy44MzIsNTUuODIyLDExLjQ5Niw0Mi42OSBjNy42NjQtMTMuMTMzLDE4LjA2Mi0yMy41MzEsMzEuMTk0LTMxLjE5NEM1NS44MjIsMy44MzIsNzAuMTY0LDAsODUuNzE0LDBzMjkuODkzLDMuODMyLDQzLjAyNCwxMS40OTYgYzEzLjEzMyw3LjY2NCwyMy41MywxOC4wNjIsMzEuMTk0LDMxLjE5NEMxNjcuNTk3LDU1LjgyMiwxNzEuNDI5LDcwLjE2NCwxNzEuNDI5LDg1LjcxNHoiLz48L3N2Zz4=)
|
||||
}
|
||||
|
||||
.map-aside-body {
|
||||
.map-fixed-header {
|
||||
@ -438,7 +247,7 @@
|
||||
padding: 10px 0;
|
||||
padding-left: 50px;
|
||||
padding-right: 20px;
|
||||
font-size: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
h3 > a {
|
||||
|
@ -1,269 +1,10 @@
|
||||
// sourced from https://github.com/CodeSeven/toastr
|
||||
// MIT license
|
||||
// Mix-ins
|
||||
.borderRadius(@radius) {
|
||||
-moz-border-radius: @radius;
|
||||
-webkit-border-radius: @radius;
|
||||
border-radius: @radius;
|
||||
.notification-bar {
|
||||
z-index: 999999;
|
||||
overflow: hidden;
|
||||
// margin: 0 0 6px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.boxShadow(@boxShadow) {
|
||||
-moz-box-shadow: @boxShadow;
|
||||
-webkit-box-shadow: @boxShadow;
|
||||
box-shadow: @boxShadow;
|
||||
}
|
||||
|
||||
.opacity(@opacity) {
|
||||
@opacityPercent: @opacity * 100;
|
||||
opacity: @opacity;
|
||||
-ms-filter: ~"progid:DXImageTransform.Microsoft.Alpha(Opacity=@{opacityPercent})";
|
||||
filter: ~"alpha(opacity=@{opacityPercent})";
|
||||
}
|
||||
|
||||
.wordWrap(@wordWrap: break-word) {
|
||||
-ms-word-wrap: @wordWrap;
|
||||
word-wrap: @wordWrap;
|
||||
}
|
||||
|
||||
// Variables
|
||||
@black: #000000;
|
||||
@grey: #999999;
|
||||
@light-grey: #CCCCCC;
|
||||
@white: #FFFFFF;
|
||||
@near-black: #030303;
|
||||
@green: #51A351;
|
||||
@red: #BD362F;
|
||||
@blue: #2F96B4;
|
||||
@orange: #F89406;
|
||||
@default-container-opacity: .8;
|
||||
|
||||
// Styles
|
||||
.toast-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
.wordWrap();
|
||||
|
||||
a,
|
||||
label {
|
||||
color: @white;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: @light-grey;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-close-button {
|
||||
position: relative;
|
||||
right: -0.3em;
|
||||
top: -0.3em;
|
||||
float: right;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: @white;
|
||||
-webkit-text-shadow: 0 1px 0 rgba(255,255,255,1);
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,1);
|
||||
.opacity(0.8);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: @black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
.opacity(0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/*Additional properties for button version
|
||||
iOS requires the button element instead of an anchor tag.
|
||||
If you want the anchor version, it requires `href="#"`.*/
|
||||
button.toast-close-button {
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
.toast-top-center {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toast-bottom-center {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toast-top-full-width {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toast-bottom-full-width {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toast-top-left {
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.toast-top-right {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.toast-bottom-right {
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
}
|
||||
|
||||
.toast-bottom-left {
|
||||
bottom: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
z-index: 999999;
|
||||
// The container should not be clickable.
|
||||
pointer-events: none;
|
||||
* {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
// The toast itself should be clickable.
|
||||
pointer-events: auto;
|
||||
overflow: hidden;
|
||||
margin: 0 0 6px;
|
||||
padding: 15px 15px 15px 50px;
|
||||
width: 300px;
|
||||
.borderRadius(3px 3px 3px 3px);
|
||||
background-position: 15px center;
|
||||
background-repeat: no-repeat;
|
||||
.boxShadow(0 0 12px @grey);
|
||||
color: @white;
|
||||
.opacity(@default-container-opacity);
|
||||
}
|
||||
|
||||
> :hover {
|
||||
.boxShadow(0 0 12px @black);
|
||||
.opacity(1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> .toast-info {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important;
|
||||
}
|
||||
|
||||
> .toast-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important;
|
||||
}
|
||||
|
||||
> .toast-success {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important;
|
||||
}
|
||||
|
||||
> .toast-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important;
|
||||
}
|
||||
|
||||
/*overrides*/
|
||||
&.toast-top-center > div,
|
||||
&.toast-bottom-center > div {
|
||||
width: 300px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&.toast-top-full-width > div,
|
||||
&.toast-bottom-full-width > div {
|
||||
width: 96%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.toast {
|
||||
background-color: @near-black;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background-color: @green;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background-color: @red;
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
background-color: @blue;
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
background-color: @orange;
|
||||
}
|
||||
|
||||
.toast-progress {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 4px;
|
||||
background-color: @black;
|
||||
.opacity(0.4);
|
||||
}
|
||||
|
||||
/*Responsive Design*/
|
||||
|
||||
@media all and (max-width: 240px) {
|
||||
#toast-container {
|
||||
|
||||
> div {
|
||||
padding: 8px 8px 8px 50px;
|
||||
width: 11em;
|
||||
}
|
||||
|
||||
& .toast-close-button {
|
||||
right: -0.2em;
|
||||
top: -0.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 241px) and (max-width: 480px) {
|
||||
#toast-container {
|
||||
> div {
|
||||
padding: 8px 8px 8px 50px;
|
||||
width: 18em;
|
||||
}
|
||||
|
||||
& .toast-close-button {
|
||||
right: -0.2em;
|
||||
top: -0.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 481px) and (max-width: 768px) {
|
||||
#toast-container {
|
||||
> div {
|
||||
padding: 15px 15px 15px 50px;
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
.notification-bar-message {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
276
client/main.js
276
client/main.js
@ -1,7 +1,5 @@
|
||||
var main = window.main || {};
|
||||
|
||||
main.mapShareKey = 'map-shares';
|
||||
|
||||
main.ga = window.ga || function() {};
|
||||
|
||||
main = (function(main, global) {
|
||||
@ -134,38 +132,6 @@ main = (function(main, global) {
|
||||
return main;
|
||||
}(main, window));
|
||||
|
||||
var lastCompleted = typeof lastCompleted !== 'undefined' ?
|
||||
lastCompleted :
|
||||
'';
|
||||
|
||||
main.getMapShares = function getMapShares() {
|
||||
var alreadyShared = JSON.parse(
|
||||
localStorage.getItem(main.mapShareKey) ||
|
||||
'[]'
|
||||
);
|
||||
|
||||
if (!alreadyShared || !Array.isArray(alreadyShared)) {
|
||||
localStorage.setItem(main.mapShareKey, JSON.stringify([]));
|
||||
alreadyShared = [];
|
||||
}
|
||||
return alreadyShared;
|
||||
};
|
||||
|
||||
main.setMapShare = function setMapShare(id) {
|
||||
var alreadyShared = main.getMapShares();
|
||||
var found = false;
|
||||
alreadyShared.forEach(function(_id) {
|
||||
if (_id === id) {
|
||||
found = true;
|
||||
}
|
||||
});
|
||||
if (!found) {
|
||||
alreadyShared.push(id);
|
||||
}
|
||||
localStorage.setItem(main.mapShareKey, JSON.stringify(alreadyShared));
|
||||
return alreadyShared;
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
const { Observable } = window.Rx;
|
||||
@ -190,250 +156,12 @@ $(document).ready(function() {
|
||||
);
|
||||
});
|
||||
|
||||
// map sharing
|
||||
var alreadyShared = main.getMapShares();
|
||||
|
||||
if (lastCompleted && alreadyShared.indexOf(lastCompleted) === -1) {
|
||||
$('div[id="' + lastCompleted + '"]')
|
||||
.parent()
|
||||
.parent()
|
||||
.removeClass('hidden');
|
||||
}
|
||||
|
||||
// on map view
|
||||
$('.map-challenge-block-share').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var challengeBlockName = $(this).children().attr('id');
|
||||
var challengeBlockEscapedName = challengeBlockName.replace(/\s/, '%20');
|
||||
var username = typeof window.username !== 'undefined' ?
|
||||
window.username :
|
||||
'';
|
||||
|
||||
var link = 'https://www.facebook.com/dialog/feed?' +
|
||||
'app_id=1644598365767721' +
|
||||
'&display=page&' +
|
||||
'caption=I%20just%20completed%20the%20' +
|
||||
challengeBlockEscapedName +
|
||||
'%20section%20on%20Free%20Code%20Camp%2E' +
|
||||
'&link=http%3A%2F%2Ffreecodecamp%2Ecom%2F' +
|
||||
username +
|
||||
'&redirect_uri=http%3A%2F%2Ffreecodecamp%2Ecom%2Fmap';
|
||||
|
||||
main.setMapShare(challengeBlockName);
|
||||
window.ga('send', 'event', 'Facebook', 'clicked', 'Shared on Facebook');
|
||||
window.location.href = link;
|
||||
});
|
||||
|
||||
function expandCaret(item) {
|
||||
$(item)
|
||||
.prev().find('.fa-caret-right')
|
||||
.removeClass('fa-caret-right').addClass('fa-caret-down');
|
||||
}
|
||||
|
||||
function collapseCaret(item) {
|
||||
$(item)
|
||||
.prev().find('.fa-caret-down')
|
||||
.removeClass('fa-caret-down').addClass('fa-caret-right');
|
||||
}
|
||||
|
||||
function expandBlock(item) {
|
||||
$(item).addClass('in').css('height', 'auto');
|
||||
expandCaret(item);
|
||||
}
|
||||
|
||||
function collapseBlock(item) {
|
||||
$(item).removeClass('in').css('height', 'auto');
|
||||
collapseCaret(item);
|
||||
}
|
||||
|
||||
$.each($('.sr-only'), function(i, span) {
|
||||
if ($(span).text() === ' Complete') {
|
||||
$(span).parents('p').addClass('manip-hidden');
|
||||
}
|
||||
});
|
||||
|
||||
$.each($('.map-collapse'), function(i, div) {
|
||||
if ($(div).find('.manip-hidden').length ===
|
||||
$(div).find('p').length) {
|
||||
collapseBlock(div);
|
||||
$(div).prev('h3').addClass('faded');
|
||||
$(div).prev('h2').addClass('faded');
|
||||
}
|
||||
});
|
||||
|
||||
var scrollTo, dashedName = localStorage.getItem('currentDashedName'),
|
||||
elemsToSearch = $('p.padded-ionic-icon a'), currOrLastChallenge;
|
||||
if (!dashedName && $('.sr-only').length) {
|
||||
elemsToSearch = $('.sr-only');
|
||||
}
|
||||
|
||||
currOrLastChallenge = elemsToSearch.filter(function() {
|
||||
if (dashedName) {
|
||||
return $(this).attr('href').match(dashedName);
|
||||
}
|
||||
return $(this).text() === ' Complete';
|
||||
});
|
||||
|
||||
if (currOrLastChallenge.length) {
|
||||
currOrLastChallenge = currOrLastChallenge[currOrLastChallenge.length - 1];
|
||||
scrollTo = $(currOrLastChallenge).offset().top - 380;
|
||||
$('html, body, .map-accordion').scrollTop(scrollTo);
|
||||
}
|
||||
|
||||
if (String(window.location).match(/\/map$/ig)) {
|
||||
$('body>.flashMessage').find('.alert').css('display', 'none');
|
||||
$('.map-fixed-header').css('top', '50px');
|
||||
}
|
||||
|
||||
// map global selectors
|
||||
var mapFilter = $('#map-filter');
|
||||
var mapShowAll = $('#showAll');
|
||||
|
||||
$('#nav-map-btn').on('click', function(event) {
|
||||
if (!(event.ctrlKey || event.metaKey)) {
|
||||
toggleMap();
|
||||
}
|
||||
});
|
||||
|
||||
$('.map-aside-action-collapse').on('click', collapseMap);
|
||||
|
||||
function showMap() {
|
||||
if (!main.isMapAsideLoad) {
|
||||
var mapAside = $('<iframe id = "map-aside-frame" >');
|
||||
mapAside.attr({
|
||||
src: '/map-aside',
|
||||
frameBorder: '0'
|
||||
});
|
||||
$('.map-aside').append(mapAside);
|
||||
if ($('body').hasClass('night')) {
|
||||
mapAside.addClass('night');
|
||||
}
|
||||
main.isMapAsideLoad = true;
|
||||
}
|
||||
$('.map-aside').removeClass('is-collapsed');
|
||||
}
|
||||
|
||||
function collapseMap() {
|
||||
$('.map-aside').addClass('is-collapsed');
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
function toggleMap() {
|
||||
var isCollapsed = $('.map-aside').hasClass('is-collapsed');
|
||||
if (isCollapsed) {
|
||||
showMap();
|
||||
} else {
|
||||
collapseMap();
|
||||
}
|
||||
}
|
||||
|
||||
mapShowAll.on('click', () => {
|
||||
var mapExpanded = mapShowAll.hasClass('active');
|
||||
if (!mapExpanded) {
|
||||
$.each($('.map-collapse:not(".in")'),
|
||||
function(i, div) {
|
||||
expandBlock(div);
|
||||
});
|
||||
mapShowAll.text('Collapse all challenges');
|
||||
return mapShowAll.addClass('active');
|
||||
} else {
|
||||
$.each($('.map-collapse.in'), function(i, div) {
|
||||
collapseBlock(div);
|
||||
});
|
||||
mapShowAll.text('Expand all challenges');
|
||||
return mapShowAll.removeClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Map live filter
|
||||
mapFilter.on('keyup', () => {
|
||||
if (mapFilter.val().length > 0) {
|
||||
var regexString = mapFilter.val().replace(/ /g, '.');
|
||||
var regex = new RegExp(regexString.split('').join('.*'), 'i');
|
||||
|
||||
// Hide/unhide challenges that match the regex
|
||||
$('.challenge-title').each((index, title) => {
|
||||
if (regex.test($(title).attr('name'))) {
|
||||
expandBlock($(title).closest('.chapterBlock'));
|
||||
expandBlock($(title).closest('.certBlock'));
|
||||
$(title).removeClass('hidden');
|
||||
} else {
|
||||
$(title).addClass('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Hide/unhide blocks with no matches
|
||||
$.each($('.chapterBlock'), function(i, div) {
|
||||
if ($(div).find('.hidden').length ===
|
||||
$(div).find('p').length) {
|
||||
$(div).addClass('hidden');
|
||||
$(div).prev('h3').addClass('hidden');
|
||||
} else {
|
||||
$(div).removeClass('hidden');
|
||||
$(div).prev('h3').removeClass('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Hide/unhide superblocks with no matches
|
||||
$.each($('.certBlock'), function(i, div) {
|
||||
if ($(div).children('#nested').children('h3.hidden').length ===
|
||||
$(div).children('#nested').children('h3').length) {
|
||||
$(div).prev('h2').addClass('hidden');
|
||||
} else {
|
||||
$(div).prev('h2').removeClass('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Display "Clear Filter" element
|
||||
if (mapFilter.next().children().hasClass('fa-search')) {
|
||||
mapFilter.next()
|
||||
.children()
|
||||
.removeClass('fa-search')
|
||||
.addClass('fa-times');
|
||||
mapFilter.next().addClass('filled');
|
||||
// Scroll to the top of the page
|
||||
$('html, body, .map-accordion').scrollTop(0);
|
||||
}
|
||||
} else {
|
||||
clearMapFilter();
|
||||
}
|
||||
|
||||
// Display not found if everything is hidden
|
||||
if ($.find('.certBlock').length ===
|
||||
$('.map-accordion').children('.hidden').length) {
|
||||
$('#noneFound').show();
|
||||
} else {
|
||||
$('#noneFound').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Give focus to the search box by default
|
||||
mapFilter.focus();
|
||||
|
||||
// Clicking the search button or x clears the map
|
||||
$('.map-buttons .input-group-addon').on('click', clearMapFilter);
|
||||
|
||||
function clearMapFilter() {
|
||||
mapFilter.val('');
|
||||
mapFilter.next().children().removeClass('fa-times').addClass('fa-search');
|
||||
mapFilter.next().removeClass('filled');
|
||||
$('.map-accordion').find('.hidden').removeClass('hidden');
|
||||
$('#noneFound').hide();
|
||||
}
|
||||
|
||||
// Clear the search on escape key
|
||||
mapFilter.on('keydown', (e) => {
|
||||
if (e.keyCode === 27) {
|
||||
e.preventDefault();
|
||||
clearMapFilter();
|
||||
}
|
||||
});
|
||||
|
||||
window.Mousetrap.bind('esc', clearMapFilter);
|
||||
|
||||
// keyboard shortcuts: open map
|
||||
window.Mousetrap.bind('g m', toggleMap);
|
||||
|
||||
function addAlert(message = '', type = 'alert-info') {
|
||||
return $('.flashMessage').append($(`
|
||||
<div class='alert ${type}'>
|
||||
@ -500,10 +228,6 @@ $(document).ready(function() {
|
||||
// Next Challenge
|
||||
window.location = '/challenges/next-challenge';
|
||||
});
|
||||
window.Mousetrap.bind('g n a', () => {
|
||||
// Account
|
||||
window.location = '/account';
|
||||
});
|
||||
window.Mousetrap.bind('g n m', () => {
|
||||
// Map
|
||||
window.location = '/map';
|
||||
|
@ -1,97 +0,0 @@
|
||||
/* eslint-disable no-eval */
|
||||
/* global importScripts, application */
|
||||
// executes the given code and handles the result
|
||||
|
||||
function importScript(url, error) {
|
||||
try {
|
||||
importScripts(url);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
function run(code, cb) {
|
||||
var err = null;
|
||||
var result = {};
|
||||
|
||||
try {
|
||||
var codeExec = runHidden(code);
|
||||
result.type = typeof codeExec;
|
||||
result.output = stringify(codeExec);
|
||||
} catch (e) {
|
||||
err = e.message;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
cb(err, null);
|
||||
} else {
|
||||
cb(null, result);
|
||||
}
|
||||
|
||||
self.close();
|
||||
}
|
||||
|
||||
|
||||
// protects even the worker scope from being accessed
|
||||
function runHidden(code) {
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
var indexedDB = null;
|
||||
var location = null;
|
||||
var navigator = null;
|
||||
var onerror = null;
|
||||
var onmessage = null;
|
||||
var performance = null;
|
||||
var self = null;
|
||||
var webkitIndexedDB = null;
|
||||
var postMessage = null;
|
||||
var close = null;
|
||||
var openDatabase = null;
|
||||
var openDatabaseSync = null;
|
||||
var webkitRequestFileSystem = null;
|
||||
var webkitRequestFileSystemSync = null;
|
||||
var webkitResolveLocalFileSystemSyncURL = null;
|
||||
var webkitResolveLocalFileSystemURL = null;
|
||||
var addEventListener = null;
|
||||
var dispatchEvent = null;
|
||||
var removeEventListener = null;
|
||||
var dump = null;
|
||||
var onoffline = null;
|
||||
var ononline = null;
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
var error = null;
|
||||
error = importScript(
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/chai/2.2.0/chai.min.js'
|
||||
);
|
||||
|
||||
|
||||
/* eslint-disable*/
|
||||
var assert = chai.assert;
|
||||
/* eslint-enable */
|
||||
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return eval(code);
|
||||
}
|
||||
|
||||
|
||||
// converts the output into a string
|
||||
function stringify(output) {
|
||||
var result;
|
||||
|
||||
if (typeof output === 'undefined') {
|
||||
result = 'undefined';
|
||||
} else if (output === null) {
|
||||
result = 'null';
|
||||
} else {
|
||||
result = JSON.stringify(output) || output.toString();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
application.setInterface({ run: run });
|
89
client/rechallenge/throwers.js
Normal file
89
client/rechallenge/throwers.js
Normal file
@ -0,0 +1,89 @@
|
||||
import { helpers, Observable } from 'rx';
|
||||
|
||||
const throwForJsHtml = {
|
||||
ext: /js|html/,
|
||||
throwers: [
|
||||
{
|
||||
name: 'multiline-comment',
|
||||
description: 'Detect if a JS multi-line comment is left open',
|
||||
thrower: function checkForComments({ contents }) {
|
||||
const openingComments = contents.match(/\/\*/gi);
|
||||
const closingComments = contents.match(/\*\//gi);
|
||||
if (
|
||||
openingComments &&
|
||||
(!closingComments || openingComments.length > closingComments.length)
|
||||
) {
|
||||
throw new Error('SyntaxError: Unfinished multi-line comment');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
name: 'nested-jQuery',
|
||||
description: 'Nested dollar sign calls breaks browsers',
|
||||
detectUnsafeJQ: /\$\s*?\(\s*?\$\s*?\)/gi,
|
||||
thrower: function checkForNestedJquery({ contents }) {
|
||||
if (contents.match(this.detectUnsafeJQ)) {
|
||||
throw new Error('Unsafe $($)');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
name: 'unfinished-function',
|
||||
description: 'lonely function keywords breaks browsers',
|
||||
detectFunctionCall: /function\s*?\(|function\s+\w+\s*?\(/gi,
|
||||
thrower: function checkForUnfinishedFunction({ contents }) {
|
||||
if (
|
||||
contents.match(/function/g) &&
|
||||
!contents.match(this.detectFunctionCall)
|
||||
) {
|
||||
throw new Error(
|
||||
'SyntaxError: Unsafe or unfinished function declaration'
|
||||
);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
name: 'unsafe console call',
|
||||
description: 'console call stops tests scripts from running',
|
||||
detectUnsafeConsoleCall: /if\s\(null\)\sconsole\.log\(1\);/gi,
|
||||
thrower: function checkForUnsafeConsole({ contents }) {
|
||||
if (contents.match(this.detectUnsafeConsoleCall)) {
|
||||
throw new Error('Invalid if (null) console.log(1); detected');
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default function throwers() {
|
||||
const source = this;
|
||||
return source.map(file$ => file$.flatMap(file => {
|
||||
if (!throwForJsHtml.ext.test(file.ext)) {
|
||||
return Observable.just(file);
|
||||
}
|
||||
return Observable.from(throwForJsHtml.throwers)
|
||||
.flatMap(context => {
|
||||
try {
|
||||
let finalObs;
|
||||
const maybeObservableOrPromise = context.thrower(file);
|
||||
if (helpers.isPromise(maybeObservableOrPromise)) {
|
||||
finalObs = Observable.fromPromise(maybeObservableOrPromise);
|
||||
} else if (Observable.isObservable(maybeObservableOrPromise)) {
|
||||
finalObs = maybeObservableOrPromise;
|
||||
} else {
|
||||
finalObs = Observable.just(maybeObservableOrPromise);
|
||||
}
|
||||
return finalObs;
|
||||
} catch (err) {
|
||||
return Observable.throw(err);
|
||||
}
|
||||
})
|
||||
// if none of the throwers throw, wait for last one
|
||||
.last({ defaultValue: null })
|
||||
// then map to the original file
|
||||
.map(file)
|
||||
// if err add it to the file
|
||||
// and return file
|
||||
.catch(err => {
|
||||
file.error = err;
|
||||
return Observable.just(file);
|
||||
});
|
||||
}));
|
||||
}
|
40
client/rechallenge/transformers.js
Normal file
40
client/rechallenge/transformers.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { Observable } from 'rx';
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import loopProtect from 'loop-protect';
|
||||
/* eslint-enable import/no-unresolved */
|
||||
|
||||
import { updateContents } from '../../common/utils/polyvinyl';
|
||||
|
||||
loopProtect.hit = function hit(line) {
|
||||
var err = 'Error: Exiting potential infinite loop at line ' +
|
||||
line +
|
||||
'. To disable loop protection, write: \n\\/\\/ noprotect\nas the first' +
|
||||
'line. Beware that if you do have an infinite loop in your code' +
|
||||
'this will crash your browser.';
|
||||
console.error(err);
|
||||
};
|
||||
|
||||
const transformersForHtmlJS = {
|
||||
ext: /html|js/,
|
||||
transformers: [
|
||||
{
|
||||
name: 'add-loop-protect',
|
||||
transformer: function addLoopProtect(file) {
|
||||
return updateContents(loopProtect(file.contents), file);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
// Observable[Observable[File]]::addLoopProtect() => Observable[String]
|
||||
export default function transformers() {
|
||||
const source = this;
|
||||
return source.map(files$ => files$.flatMap(file => {
|
||||
if (!transformersForHtmlJS.ext.test(file.ext)) {
|
||||
return Observable.just(file);
|
||||
}
|
||||
return Observable.from(transformersForHtmlJS.transformers)
|
||||
.reduce((file, context) => context.transformer(file), file);
|
||||
}));
|
||||
}
|
43
client/sagas/analytics-saga.js
Normal file
43
client/sagas/analytics-saga.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { Observable } from 'rx';
|
||||
import { createErrorObservable } from '../../common/app/redux/actions';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
|
||||
// analytics types
|
||||
// interface social {
|
||||
// network: String, // facebook, twitter, etc
|
||||
// action: String, // like, favorite, etc
|
||||
// target: String // url like fcc.com or any other string
|
||||
// }
|
||||
// interface event {
|
||||
// category: String,
|
||||
// action: String,
|
||||
// label?: String,
|
||||
// value?: String
|
||||
// }
|
||||
//
|
||||
const types = [ 'event', 'social' ];
|
||||
function formatFields({ type, ...fields }) {
|
||||
// make sure type is supported
|
||||
if (!types.some(_type => _type === type)) {
|
||||
return null;
|
||||
}
|
||||
return Object.keys(fields).reduce((_fields, field) => {
|
||||
_fields[ type + capitalize(field) ] = fields[ field ];
|
||||
return _fields;
|
||||
}, { type });
|
||||
}
|
||||
|
||||
export default function analyticsSaga(actions, getState, { window }) {
|
||||
const { ga } = window;
|
||||
if (typeof ga !== 'function') {
|
||||
console.log('GA not found');
|
||||
return Observable.empty();
|
||||
}
|
||||
return actions
|
||||
.filter(({ meta }) => !!(meta && meta.analytics && meta.analytics.type))
|
||||
.map(({ meta: { analytics } }) => formatFields(analytics))
|
||||
.filter(Boolean)
|
||||
// ga always returns undefined
|
||||
.map(({ type, ...fields }) => ga('send', type, fields))
|
||||
.catch(createErrorObservable);
|
||||
}
|
51
client/sagas/code-storage-saga.js
Normal file
51
client/sagas/code-storage-saga.js
Normal file
@ -0,0 +1,51 @@
|
||||
import store from 'store';
|
||||
|
||||
import types from '../../common/app/routes/challenges/redux/types';
|
||||
import {
|
||||
savedCodeFound
|
||||
} from '../../common/app/routes/challenges/redux/actions';
|
||||
|
||||
const legecyPrefixes = [
|
||||
'Bonfire: ',
|
||||
'Waypoint: ',
|
||||
'Zipline: ',
|
||||
'Basejump: ',
|
||||
'Checkpoint: '
|
||||
];
|
||||
|
||||
function getCode(id, legacy) {
|
||||
if (store.has(id)) {
|
||||
return store.get(id);
|
||||
}
|
||||
if (store.has(legacy)) {
|
||||
const code = '' + store.get(legacy);
|
||||
store.remove(legacy);
|
||||
return code;
|
||||
}
|
||||
return legecyPrefixes.reduce((code, prefix) => {
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
return store.get(prefix + legacy + 'Val');
|
||||
}, null);
|
||||
}
|
||||
|
||||
export default function codeStorageSaga(actions$, getState) {
|
||||
return actions$
|
||||
.filter(({ type }) => (
|
||||
type === types.saveCode ||
|
||||
type === types.loadCode
|
||||
))
|
||||
.map(({ type }) => {
|
||||
const { id = '', files = {}, legacyKey = '' } = getState().challengesApp;
|
||||
if (type === types.saveCode) {
|
||||
store.set(id, files);
|
||||
return null;
|
||||
}
|
||||
const codeFound = getCode(id, legacyKey);
|
||||
if (codeFound) {
|
||||
return savedCodeFound(codeFound);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
@ -1,20 +1,14 @@
|
||||
// () =>
|
||||
// (store: Store) =>
|
||||
// (next: (action: Action) => Object) =>
|
||||
// errSaga(action: Action) => Object|Void
|
||||
export default () => ({ dispatch }) => next => {
|
||||
return function errorSaga(action) {
|
||||
const result = next(action);
|
||||
if (!action.error) { return result; }
|
||||
|
||||
console.error(action.error);
|
||||
return dispatch({
|
||||
export default function errorSaga(action$) {
|
||||
return action$
|
||||
.filter(({ error }) => !!error)
|
||||
.map(({ error }) => error)
|
||||
.doOnNext(error => console.error(error))
|
||||
.map(() => ({
|
||||
type: 'app.makeToast',
|
||||
payload: {
|
||||
type: 'error',
|
||||
title: 'Oops, something went wrong',
|
||||
message: 'Something went wrong, please try again later'
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
167
client/sagas/execute-challenge-saga.js
Normal file
167
client/sagas/execute-challenge-saga.js
Normal file
@ -0,0 +1,167 @@
|
||||
import { Scheduler, Observable } from 'rx';
|
||||
|
||||
import {
|
||||
challengeSelector
|
||||
} from '../../common/app/routes/challenges/redux/selectors';
|
||||
import { ajax$ } from '../../common/utils/ajax-stream';
|
||||
import throwers from '../rechallenge/throwers';
|
||||
import transformers from '../rechallenge/transformers';
|
||||
import types from '../../common/app/routes/challenges/redux/types';
|
||||
import { createErrorObservable } from '../../common/app/redux/actions';
|
||||
import {
|
||||
frameMain,
|
||||
frameTests,
|
||||
initOutput,
|
||||
saveCode
|
||||
} from '../../common/app/routes/challenges/redux/actions';
|
||||
import { setExt, updateContents } from '../../common/utils/polyvinyl';
|
||||
|
||||
// createFileStream(files: Dictionary[Path, PolyVinyl]) =>
|
||||
// Observable[...Observable[...PolyVinyl]]
|
||||
function createFileStream(files = {}) {
|
||||
return Observable.just(
|
||||
Observable.from(Object.keys(files)).map(key => files[key])
|
||||
);
|
||||
}
|
||||
|
||||
const globalRequires = [{
|
||||
link: 'https://cdnjs.cloudflare.com/' +
|
||||
'ajax/libs/normalize/4.2.0/normalize.min.css'
|
||||
}, {
|
||||
src: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.js'
|
||||
}];
|
||||
|
||||
const scriptCache = new Map();
|
||||
const linkCache = new Map();
|
||||
|
||||
function cacheScript({ src } = {}, crossDomain = true) {
|
||||
if (!src) {
|
||||
throw new Error('No source provided for script');
|
||||
}
|
||||
if (scriptCache.has(src)) {
|
||||
return scriptCache.get(src);
|
||||
}
|
||||
const script$ = ajax$({ url: src, crossDomain })
|
||||
.doOnNext(res => {
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Request errror: ' + res.status);
|
||||
}
|
||||
})
|
||||
.map(({ response }) => response)
|
||||
.map(script => `<script>${script}</script>`)
|
||||
.shareReplay();
|
||||
|
||||
scriptCache.set(src, script$);
|
||||
return script$;
|
||||
}
|
||||
|
||||
function cacheLink({ link } = {}, crossDomain = true) {
|
||||
if (!link) {
|
||||
return Observable.throw(new Error('No source provided for link'));
|
||||
}
|
||||
if (linkCache.has(link)) {
|
||||
return linkCache.get(link);
|
||||
}
|
||||
const link$ = ajax$({ url: link, crossDomain })
|
||||
.doOnNext(res => {
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Request errror: ' + res.status);
|
||||
}
|
||||
})
|
||||
.map(({ response }) => response)
|
||||
.map(script => `<style>${script}</style>`)
|
||||
.catch(() => Observable.just(''))
|
||||
.shareReplay();
|
||||
|
||||
linkCache.set(link, link$);
|
||||
return link$;
|
||||
}
|
||||
|
||||
|
||||
const htmlCatch = '\n<!--fcc-->';
|
||||
const jsCatch = '\n;/*fcc*/';
|
||||
|
||||
export default function executeChallengeSaga(action$, getState) {
|
||||
const frameRunner$ = cacheScript(
|
||||
{ src: '/js/frame-runner.js' },
|
||||
false
|
||||
);
|
||||
return action$
|
||||
.filter(({ type }) => (
|
||||
type === types.executeChallenge ||
|
||||
type === types.updateMain
|
||||
))
|
||||
.debounce(750)
|
||||
.flatMapLatest(({ type }) => {
|
||||
const state = getState();
|
||||
const { files } = state.challengesApp;
|
||||
const { challenge: { required = [] } } = challengeSelector(state);
|
||||
const finalRequires = [...globalRequires, ...required ];
|
||||
return createFileStream(files)
|
||||
::throwers()
|
||||
::transformers()
|
||||
// createbuild
|
||||
.flatMap(file$ => file$.reduce((build, file) => {
|
||||
let finalFile;
|
||||
if (file.ext === 'js') {
|
||||
finalFile = setExt('html', updateContents(
|
||||
`<script>${file.contents}${jsCatch}</script>`,
|
||||
file
|
||||
));
|
||||
} else if (file.ext === 'css') {
|
||||
finalFile = setExt('html', updateContents(
|
||||
`<style>${file.contents}</style>`,
|
||||
file
|
||||
));
|
||||
} else {
|
||||
finalFile = file;
|
||||
}
|
||||
return build + finalFile.contents + htmlCatch;
|
||||
}, ''))
|
||||
// add required scripts and links here
|
||||
.flatMap(source => {
|
||||
const head$ = Observable.from(finalRequires)
|
||||
.flatMap(required => {
|
||||
if (required.src) {
|
||||
return cacheScript(required, required.crossDomain);
|
||||
}
|
||||
if (required.link) {
|
||||
return cacheLink(required, required.crossDomain);
|
||||
}
|
||||
return Observable.just('');
|
||||
})
|
||||
.reduce((head, required) => head + required, '')
|
||||
.map(head => `<head>${head}</head>`);
|
||||
|
||||
return Observable.combineLatest(head$, frameRunner$)
|
||||
.map(([ head, frameRunner ]) => {
|
||||
const body = `
|
||||
<body>
|
||||
<!-- fcc-start-source -->
|
||||
${source}
|
||||
<!-- fcc-end-source -->
|
||||
</body>`;
|
||||
return {
|
||||
build: head + body + frameRunner,
|
||||
source,
|
||||
head
|
||||
};
|
||||
});
|
||||
})
|
||||
.flatMap(payload => {
|
||||
const actions = [
|
||||
frameMain(payload)
|
||||
];
|
||||
if (type === types.executeChallenge) {
|
||||
actions.push(saveCode(), frameTests(payload));
|
||||
}
|
||||
return Observable.from(actions, null, null, Scheduler.default);
|
||||
})
|
||||
.startWith((
|
||||
type === types.executeChallenge ?
|
||||
initOutput('// running test') :
|
||||
null
|
||||
))
|
||||
.catch(createErrorObservable);
|
||||
});
|
||||
}
|
120
client/sagas/frame-saga.js
Normal file
120
client/sagas/frame-saga.js
Normal file
@ -0,0 +1,120 @@
|
||||
import Rx, { Observable, Subject } from 'rx';
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import loopProtect from 'loop-protect';
|
||||
/* eslint-enable import/no-unresolved */
|
||||
import types from '../../common/app/routes/challenges/redux/types';
|
||||
import {
|
||||
updateOutput,
|
||||
checkChallenge,
|
||||
updateTests
|
||||
} from '../../common/app/routes/challenges/redux/actions';
|
||||
|
||||
// we use three different frames to make them all essentially pure functions
|
||||
const mainId = 'fcc-main-frame';
|
||||
const testId = 'fcc-test-frame';
|
||||
|
||||
const createHeader = (id = mainId) => `
|
||||
<script>
|
||||
window.__frameId = '${id}';
|
||||
</script>
|
||||
`;
|
||||
|
||||
|
||||
function createFrame(document, id = mainId) {
|
||||
const frame = document.createElement('iframe');
|
||||
frame.id = id;
|
||||
frame.setAttribute('style', 'display: none');
|
||||
document.body.appendChild(frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
function refreshFrame(frame) {
|
||||
frame.src = 'about:blank';
|
||||
return frame;
|
||||
}
|
||||
|
||||
function getFrameDocument(document, id = mainId) {
|
||||
let frame = document.getElementById(id);
|
||||
if (!frame) {
|
||||
frame = createFrame(document, id);
|
||||
}
|
||||
frame.contentWindow.loopProtect = loopProtect;
|
||||
return {
|
||||
frame: frame.contentDocument || frame.contentWindow.document,
|
||||
frameWindow: frame.contentWindow
|
||||
};
|
||||
}
|
||||
|
||||
const consoleReg = /(?:\b)console(\.log\S+)/g;
|
||||
const sourceReg =
|
||||
/(<!-- fcc-start-source -->)([\s\S]*?)(?=<!-- fcc-end-source -->)/g;
|
||||
function proxyConsole(build, source) {
|
||||
const newSource = source.replace(consoleReg, (match, methodCall) => {
|
||||
return 'window.__console' + methodCall;
|
||||
});
|
||||
return build.replace(sourceReg, '\$1' + newSource);
|
||||
}
|
||||
|
||||
function buildProxyConsole(window, proxyLogger$) {
|
||||
const oldLog = window.console.log.bind(console);
|
||||
window.__console = {};
|
||||
window.__console.log = function proxyConsole(...args) {
|
||||
proxyLogger$.onNext(args);
|
||||
return oldLog(...args);
|
||||
};
|
||||
}
|
||||
|
||||
function frameMain({ build, source } = {}, document, proxyLogger$) {
|
||||
const { frame: main, frameWindow } = getFrameDocument(document);
|
||||
refreshFrame(main);
|
||||
buildProxyConsole(frameWindow, proxyLogger$);
|
||||
main.open();
|
||||
main.write(createHeader() + proxyConsole(build, source));
|
||||
main.close();
|
||||
}
|
||||
|
||||
function frameTests({ build, source } = {}, document) {
|
||||
const { frame: tests } = getFrameDocument(document, testId);
|
||||
refreshFrame(tests);
|
||||
tests.Rx = Rx;
|
||||
tests.__source = source;
|
||||
tests.open();
|
||||
tests.write(createHeader(testId) + build);
|
||||
tests.close();
|
||||
}
|
||||
|
||||
export default function frameSaga(actions$, getState, { window, document }) {
|
||||
window.__common = {};
|
||||
window.__common.shouldRun = () => true;
|
||||
const proxyLogger$ = new Subject();
|
||||
const runTests$ = window.__common[testId + 'Ready$'] =
|
||||
new Subject();
|
||||
const result$ = actions$
|
||||
.filter(({ type }) => (
|
||||
type === types.frameMain ||
|
||||
type === types.frameTests ||
|
||||
type === types.frameOutput
|
||||
))
|
||||
.map(action => {
|
||||
if (action.type === types.frameMain) {
|
||||
return frameMain(action.payload, document, proxyLogger$);
|
||||
}
|
||||
return frameTests(action.payload, document);
|
||||
});
|
||||
|
||||
return Observable.merge(
|
||||
proxyLogger$.map(updateOutput),
|
||||
runTests$.flatMap(() => {
|
||||
const { frame } = getFrameDocument(document, testId);
|
||||
const { tests } = getState().challengesApp;
|
||||
const postTests = Observable.of(
|
||||
updateOutput('// tests completed'),
|
||||
checkChallenge()
|
||||
).delay(250);
|
||||
return frame.__runTests$(tests)
|
||||
.map(updateTests)
|
||||
.concat(postTests);
|
||||
}),
|
||||
result$
|
||||
);
|
||||
}
|
178
client/sagas/gitter-saga.js
Normal file
178
client/sagas/gitter-saga.js
Normal file
@ -0,0 +1,178 @@
|
||||
import { Subject, Observable } from 'rx';
|
||||
import Chat from 'gitter-sidecar';
|
||||
import types from '../../common/app/redux/types';
|
||||
import {
|
||||
openHelpChat,
|
||||
closeHelpChat,
|
||||
toggleMainChat
|
||||
} from '../../common/app/redux/actions';
|
||||
|
||||
export function createHeader(room, title, document) {
|
||||
const type = room === 'freecodecamp' ? 'main' : 'help';
|
||||
const div = document.createElement('div');
|
||||
const span = document.createElement('span');
|
||||
const actionBar = document.querySelector(
|
||||
`#chat-embed-${type}> .gitter-chat-embed-action-bar`
|
||||
);
|
||||
span.appendChild(document.createTextNode(title));
|
||||
div.className = `chat-embed-${type}-title`;
|
||||
div.appendChild(span);
|
||||
actionBar.insertBefore(div, actionBar.firstChild);
|
||||
}
|
||||
|
||||
export function createHelpContainer(document) {
|
||||
const container = document.createElement('aside');
|
||||
container.id = 'chat-embed-help';
|
||||
container.className = 'gitter-chat-embed is-collapsed';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
export function createMainChat(getState, document) {
|
||||
let mainChatTitleAdded = false;
|
||||
const mainChatContainer = document.createElement('aside');
|
||||
mainChatContainer.id = 'chat-embed-main';
|
||||
mainChatContainer.className = 'gitter-chat-embed is-collapsed';
|
||||
document.body.appendChild(mainChatContainer);
|
||||
const mainChat = new Chat({
|
||||
room: 'freecodecamp/freecodecamp',
|
||||
activationElement: false,
|
||||
targetElement: mainChatContainer
|
||||
});
|
||||
|
||||
const toggle$ = Observable.fromEventPattern(
|
||||
h => mainChatContainer.addEventListener('gitter-chat-toggle', h),
|
||||
h => mainChatContainer.removeEventListener('gitter-chat-toggle', h)
|
||||
)
|
||||
.map(e => {
|
||||
const { isMainChatOpen } = getState().app;
|
||||
if (!mainChatTitleAdded) {
|
||||
mainChatTitleAdded = true;
|
||||
createHeader('freecodecamp', 'Free Code Camp\'s Main Chat', document);
|
||||
}
|
||||
if (isMainChatOpen === e.detail.state) {
|
||||
return null;
|
||||
}
|
||||
return toggleMainChat();
|
||||
});
|
||||
return {
|
||||
mainChat,
|
||||
toggle$
|
||||
};
|
||||
}
|
||||
|
||||
// only one help room may be alive at once
|
||||
export function createHelpChat(room, container, proxy, document) {
|
||||
const title = room.replace(/([A-Z])/g, ' $1');
|
||||
let isTitleAdded = false;
|
||||
const chat = new Chat({
|
||||
room: `freecodecamp/${room}`,
|
||||
activationElement: false,
|
||||
targetElement: container
|
||||
});
|
||||
// return subscription to toggle stream
|
||||
// dispose when rooms switch
|
||||
const subscription = Observable.fromEventPattern(
|
||||
h => container.addEventListener('gitter-chat-toggle', h),
|
||||
h => container.removeEventListener('gitter-chat-toggle', h)
|
||||
)
|
||||
.map(e => {
|
||||
if (!isTitleAdded) {
|
||||
isTitleAdded = true;
|
||||
createHeader(room, title, document);
|
||||
}
|
||||
const gitterState = e.detail.state;
|
||||
return gitterState ? openHelpChat() : closeHelpChat();
|
||||
})
|
||||
// use subject proxy to dispatch actions
|
||||
.subscribe(proxy);
|
||||
return { chat, subscription };
|
||||
}
|
||||
|
||||
export const cache = {};
|
||||
export function toggleHelpChat(isOpen, room, proxy, document) {
|
||||
// check is container is already created
|
||||
if (!cache['container']) {
|
||||
cache['container'] = createHelpContainer(document);
|
||||
}
|
||||
const { container } = cache;
|
||||
if (!cache['chat']) {
|
||||
const {
|
||||
chat,
|
||||
subscription
|
||||
} = createHelpChat(room, container, proxy, document);
|
||||
cache.chat = chat;
|
||||
// make sure we clear out old subscription
|
||||
if (cache.subscription && cache.subscription.dispose) {
|
||||
cache.subscription.dispose();
|
||||
}
|
||||
cache.subscription = subscription;
|
||||
cache.currentRoom = room;
|
||||
}
|
||||
// have we switched rooms?
|
||||
if (!cache.currentRoom === room) {
|
||||
// room has changed, if chat object exist, destroy it
|
||||
// and end subscription to toggle
|
||||
try {
|
||||
cache.chat.destroy();
|
||||
cache.subscription.dispose();
|
||||
// chat and subscription may not exist at first so we catch errors here
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
// create new chat room and cache
|
||||
const {
|
||||
chat,
|
||||
subscription
|
||||
} = createHelpChat(room, container, proxy, document);
|
||||
cache.chat = chat;
|
||||
cache.subscription = subscription;
|
||||
cache.currentRoom = room;
|
||||
}
|
||||
// all goes well pull chat object from cache
|
||||
const { chat } = cache;
|
||||
chat.toggleChat(isOpen);
|
||||
}
|
||||
|
||||
export default function gitterSaga(actions$, getState, { document }) {
|
||||
const helpToggleProxy = new Subject();
|
||||
const {
|
||||
mainChat,
|
||||
toggle$: mainChatToggle$
|
||||
} = createMainChat(getState, document);
|
||||
return Observable.merge(
|
||||
mainChatToggle$,
|
||||
helpToggleProxy,
|
||||
actions$
|
||||
.filter(({ type }) => (
|
||||
type === types.openMainChat ||
|
||||
type === types.closeMainChat ||
|
||||
type === types.toggleMainChat ||
|
||||
type === types.toggleHelpChat
|
||||
))
|
||||
.map(({ type }) => {
|
||||
const state = getState();
|
||||
let shouldBlur = false;
|
||||
if (type === types.toggleHelpChat) {
|
||||
const {
|
||||
app: { isHelpChatOpen },
|
||||
challengesApp: { helpChatRoom }
|
||||
} = state;
|
||||
shouldBlur = !isHelpChatOpen;
|
||||
toggleHelpChat(
|
||||
isHelpChatOpen,
|
||||
helpChatRoom,
|
||||
helpToggleProxy,
|
||||
document
|
||||
);
|
||||
}
|
||||
const { isMainChatOpen } = state.app;
|
||||
mainChat.toggleChat(isMainChatOpen);
|
||||
shouldBlur = !isMainChatOpen;
|
||||
if (!shouldBlur) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
}
|
@ -1,24 +1,11 @@
|
||||
import { hardGoTo } from '../../common/app/redux/types';
|
||||
import types from '../../common/app/redux/types';
|
||||
|
||||
const loc = typeof window !== 'undefined' ?
|
||||
window.location :
|
||||
{};
|
||||
|
||||
export default () => ({ dispatch }) => next => {
|
||||
return function hardGoToSaga(action) {
|
||||
const result = next(action);
|
||||
if (action.type !== hardGoTo) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!loc.pathname) {
|
||||
dispatch({
|
||||
type: 'app.error',
|
||||
error: new Error('no location object found')
|
||||
});
|
||||
}
|
||||
|
||||
loc.pathname = action.payload || '/map';
|
||||
return null;
|
||||
};
|
||||
};
|
||||
const { hardGoTo } = types;
|
||||
export default function hardGoToSaga(action$, getState, { history }) {
|
||||
return action$
|
||||
.filter(({ type }) => type === hardGoTo)
|
||||
.map(({ payload = '/settings' }) => {
|
||||
history.pushState(history.state, null, payload);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,23 @@
|
||||
import errSaga from './err-saga';
|
||||
import titleSaga from './title-saga';
|
||||
import localStorageSaga from './local-storage-saga';
|
||||
import hardGoToSaga from './hard-go-to-saga';
|
||||
import windowSaga from './window-saga';
|
||||
import executeChallengeSaga from './execute-challenge-saga';
|
||||
import frameSaga from './frame-saga';
|
||||
import codeStorageSaga from './code-storage-saga';
|
||||
import gitterSaga from './gitter-saga';
|
||||
import mouseTrapSaga from './mouse-trap-saga';
|
||||
import analyticsSaga from './analytics-saga';
|
||||
|
||||
export default [ errSaga, titleSaga, localStorageSaga, hardGoToSaga ];
|
||||
export default [
|
||||
errSaga,
|
||||
titleSaga,
|
||||
hardGoToSaga,
|
||||
windowSaga,
|
||||
executeChallengeSaga,
|
||||
frameSaga,
|
||||
codeStorageSaga,
|
||||
gitterSaga,
|
||||
mouseTrapSaga,
|
||||
analyticsSaga
|
||||
];
|
||||
|
@ -1,69 +0,0 @@
|
||||
import {
|
||||
saveForm,
|
||||
clearForm,
|
||||
loadSavedForm
|
||||
} from '../../common/app/routes/Jobs/redux/types';
|
||||
|
||||
import {
|
||||
saveCompleted,
|
||||
loadSavedFormCompleted
|
||||
} from '../../common/app/routes/Jobs/redux/actions';
|
||||
|
||||
const formKey = 'newJob';
|
||||
let enabled = false;
|
||||
let store = typeof window !== 'undefined' ?
|
||||
window.localStorage :
|
||||
false;
|
||||
|
||||
try {
|
||||
const testKey = '__testKey__';
|
||||
store.setItem(testKey, testKey);
|
||||
enabled = store.getItem(testKey) === testKey;
|
||||
store.removeItem(testKey);
|
||||
} catch (e) {
|
||||
enabled = !e;
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
console.error(new Error('No localStorage found'));
|
||||
}
|
||||
|
||||
export default () => ({ dispatch }) => next => {
|
||||
return function localStorageSaga(action) {
|
||||
if (!enabled) { return next(action); }
|
||||
|
||||
if (action.type === saveForm) {
|
||||
const form = action.payload;
|
||||
try {
|
||||
store.setItem(formKey, JSON.stringify(form));
|
||||
next(action);
|
||||
return dispatch(saveCompleted(form));
|
||||
} catch (error) {
|
||||
return dispatch({
|
||||
type: 'app.handleError',
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === clearForm) {
|
||||
store.removeItem(formKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (action.type === loadSavedForm) {
|
||||
const formString = store.getItem(formKey);
|
||||
try {
|
||||
const form = JSON.parse(formString);
|
||||
return dispatch(loadSavedFormCompleted(form));
|
||||
} catch (error) {
|
||||
return dispatch({
|
||||
type: 'app.handleError',
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
||||
};
|
41
client/sagas/mouse-trap-saga.js
Normal file
41
client/sagas/mouse-trap-saga.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { Observable } from 'rx';
|
||||
import MouseTrap from 'mousetrap';
|
||||
import { push } from 'react-router-redux';
|
||||
import {
|
||||
toggleNightMode,
|
||||
toggleMapDrawer,
|
||||
toggleMainChat,
|
||||
hardGoTo
|
||||
} from '../../common/app/redux/actions';
|
||||
|
||||
function bindKey$(key, actionCreator) {
|
||||
return Observable.fromEventPattern(
|
||||
h => MouseTrap.bind(key, h),
|
||||
h => MouseTrap.unbind(key, h)
|
||||
)
|
||||
.map(actionCreator);
|
||||
}
|
||||
|
||||
const softRedirects = {
|
||||
'g n n': '/challenges/next-challenge',
|
||||
'g n a': '/about',
|
||||
'g n m': '/map',
|
||||
'g n w': '/wiki',
|
||||
'g n s': '/shop',
|
||||
'g n o': '/settings'
|
||||
};
|
||||
|
||||
export default function mouseTrapSaga(actions$) {
|
||||
const traps$ = [
|
||||
...Object.keys(softRedirects)
|
||||
.map(key => bindKey$(key, () => push(softRedirects[key]))),
|
||||
bindKey$(
|
||||
'g n r',
|
||||
() => hardGoTo('https://github.com/freecodecamp/freecodecamp')
|
||||
),
|
||||
bindKey$('g m', toggleMapDrawer),
|
||||
bindKey$('g t n', toggleNightMode),
|
||||
bindKey$('g c', toggleMainChat)
|
||||
];
|
||||
return Observable.merge(traps$).takeUntil(actions$.last());
|
||||
}
|
@ -1,17 +1,10 @@
|
||||
// (doc: Object) =>
|
||||
// () =>
|
||||
// (next: (action: Action) => Object) =>
|
||||
// titleSage(action: Action) => Object|Void
|
||||
export default ({ doc }) => ({ getState }) => next => {
|
||||
return function titleSage(action) {
|
||||
// get next state
|
||||
const result = next(action);
|
||||
if (action.type !== 'app.updateTitle') {
|
||||
return result;
|
||||
}
|
||||
const state = getState();
|
||||
const newTitle = state.app.title;
|
||||
doc.title = newTitle;
|
||||
return result;
|
||||
};
|
||||
};
|
||||
export default function titleSage(action$, getState, { document }) {
|
||||
return action$
|
||||
.filter(action => action.type === 'app.updateTitle')
|
||||
.map(() => {
|
||||
const state = getState();
|
||||
const newTitle = state.app.title;
|
||||
document.title = newTitle;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
35
client/sagas/window-saga.js
Normal file
35
client/sagas/window-saga.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { Observable } from 'rx';
|
||||
import types from '../../common/app/redux/types';
|
||||
import { updateWindowHeight } from '../../common/app/redux/actions';
|
||||
|
||||
const { initWindowHeight } = types;
|
||||
function getWindowSize(document, window) {
|
||||
const body = document.getElementsByTagName('body')[0];
|
||||
return window.innerHeight ||
|
||||
document.docElement.clientHeight ||
|
||||
body.clientHeight ||
|
||||
0;
|
||||
}
|
||||
|
||||
function listenForResize(document, window) {
|
||||
return Observable.fromEvent(window, 'resize')
|
||||
.debounce(250)
|
||||
.startWith({})
|
||||
.map(() => getWindowSize(document, window));
|
||||
}
|
||||
|
||||
export default function windowSaga(
|
||||
action$,
|
||||
getState,
|
||||
{ isDev, document, window }
|
||||
) {
|
||||
return action$
|
||||
.filter(({ type }) => type === initWindowHeight)
|
||||
.flatMap(() => {
|
||||
if (isDev) {
|
||||
return listenForResize(document, window);
|
||||
}
|
||||
return Observable.just(getWindowSize(document, window));
|
||||
})
|
||||
.map(updateWindowHeight);
|
||||
}
|
6
client/utils/send-page-analytics.js
Normal file
6
client/utils/send-page-analytics.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default function sendPageAnalytics(history, ga) {
|
||||
history.listen(location => {
|
||||
ga('set', 'page', location.pathname + location.search);
|
||||
ga('send', 'pageview');
|
||||
});
|
||||
}
|
44
client/utils/use-lang-routes.js
Normal file
44
client/utils/use-lang-routes.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { addLang, getLangFromPath } from '../../common/app/utils/lang.js';
|
||||
|
||||
function addLangToLocation(location, lang) {
|
||||
if (!location) {
|
||||
return location;
|
||||
}
|
||||
if (typeof location === 'string') {
|
||||
return addLang(location, lang);
|
||||
}
|
||||
return {
|
||||
...location,
|
||||
pathname: addLang(location.pathname, lang)
|
||||
};
|
||||
}
|
||||
|
||||
function getLangFromLocation(location) {
|
||||
if (!location) {
|
||||
return location;
|
||||
}
|
||||
if (typeof location === 'string') {
|
||||
return getLangFromPath(location);
|
||||
}
|
||||
return getLangFromPath(location.pathname);
|
||||
}
|
||||
|
||||
export default function useLangRoutes(createHistory) {
|
||||
return (options = {}) => {
|
||||
let lang = 'en';
|
||||
const history = createHistory(options);
|
||||
const unsubscribeFromHistory = history.listen(nextLocation => {
|
||||
lang = getLangFromLocation(nextLocation);
|
||||
});
|
||||
const push = location => history.push(addLangToLocation(location, lang));
|
||||
const replace = location => history.replace(
|
||||
addLangToLocation(location, lang)
|
||||
);
|
||||
return {
|
||||
...history,
|
||||
push,
|
||||
replace,
|
||||
unsubscribe() { unsubscribeFromHistory(); }
|
||||
};
|
||||
};
|
||||
}
|
@ -1,68 +1,142 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Row } from 'react-bootstrap';
|
||||
import { ToastMessage, ToastContainer } from 'react-toastr';
|
||||
import { compose } from 'redux';
|
||||
import { Button, Row } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { fetchUser } from './redux/actions';
|
||||
import contain from './utils/professor-x';
|
||||
import MapDrawer from './components/Map-Drawer.jsx';
|
||||
import {
|
||||
fetchUser,
|
||||
initWindowHeight,
|
||||
updateNavHeight,
|
||||
toggleMapDrawer,
|
||||
toggleMainChat,
|
||||
updateAppLang,
|
||||
trackEvent
|
||||
} from './redux/actions';
|
||||
|
||||
import { submitChallenge } from './routes/challenges/redux/actions';
|
||||
|
||||
import Nav from './components/Nav';
|
||||
import Toasts from './toasts/Toasts.jsx';
|
||||
import { userSelector } from './redux/selectors';
|
||||
|
||||
const toastMessageFactory = React.createFactory(ToastMessage.animation);
|
||||
const bindableActions = {
|
||||
initWindowHeight,
|
||||
updateNavHeight,
|
||||
fetchUser,
|
||||
submitChallenge,
|
||||
toggleMapDrawer,
|
||||
toggleMainChat,
|
||||
updateAppLang,
|
||||
trackEvent
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
state => state.app,
|
||||
({
|
||||
userSelector,
|
||||
state => state.app.shouldShowSignIn,
|
||||
state => state.app.toast,
|
||||
state => state.app.isMapDrawerOpen,
|
||||
state => state.app.isMapAlreadyLoaded,
|
||||
state => state.challengesApp.toast,
|
||||
(
|
||||
{ user: { username, points, picture } },
|
||||
shouldShowSignIn,
|
||||
toast,
|
||||
isMapDrawerOpen,
|
||||
isMapAlreadyLoaded,
|
||||
) => ({
|
||||
username,
|
||||
points,
|
||||
picture,
|
||||
toast
|
||||
}) => ({
|
||||
username,
|
||||
points,
|
||||
picture,
|
||||
toast
|
||||
toast,
|
||||
shouldShowSignIn,
|
||||
isMapDrawerOpen,
|
||||
isMapAlreadyLoaded,
|
||||
isSignedIn: !!username
|
||||
})
|
||||
);
|
||||
|
||||
const fetchContainerOptions = {
|
||||
fetchAction: 'fetchUser',
|
||||
isPrimed({ username }) {
|
||||
return !!username;
|
||||
}
|
||||
};
|
||||
|
||||
// export plain class for testing
|
||||
export class FreeCodeCamp extends React.Component {
|
||||
static displayName = 'FreeCodeCamp';
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object
|
||||
};
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
username: PropTypes.string,
|
||||
isSignedIn: PropTypes.bool,
|
||||
points: PropTypes.number,
|
||||
picture: PropTypes.string,
|
||||
toast: PropTypes.object
|
||||
toast: PropTypes.object,
|
||||
updateNavHeight: PropTypes.func,
|
||||
initWindowHeight: PropTypes.func,
|
||||
submitChallenge: PropTypes.func,
|
||||
isMapDrawerOpen: PropTypes.bool,
|
||||
isMapAlreadyLoaded: PropTypes.bool,
|
||||
toggleMapDrawer: PropTypes.func,
|
||||
toggleMainChat: PropTypes.func,
|
||||
fetchUser: PropTypes.func,
|
||||
shouldShowSignIn: PropTypes.bool,
|
||||
params: PropTypes.object,
|
||||
updateAppLang: PropTypes.func.isRequired,
|
||||
trackEvent: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
componentWillReceiveProps({ toast: nextToast = {} }) {
|
||||
const { toast = {} } = this.props;
|
||||
if (toast.id !== nextToast.id) {
|
||||
this.refs.toaster[nextToast.type || 'success'](
|
||||
nextToast.message,
|
||||
nextToast.title,
|
||||
{
|
||||
closeButton: true,
|
||||
timeOut: 10000
|
||||
}
|
||||
);
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.params.lang !== nextProps.params.lang) {
|
||||
this.props.updateAppLang(nextProps.params.lang);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.initWindowHeight();
|
||||
if (!this.props.isSignedIn) {
|
||||
this.props.fetchUser();
|
||||
}
|
||||
}
|
||||
|
||||
renderChallengeComplete() {
|
||||
const { submitChallenge } = this.props;
|
||||
return (
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='small'
|
||||
bsStyle='primary'
|
||||
className='animated fadeIn'
|
||||
onClick={ submitChallenge }
|
||||
>
|
||||
Submit and go to my next challenge
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { username, points, picture } = this.props;
|
||||
const navProps = { username, points, picture };
|
||||
const { router } = this.context;
|
||||
const {
|
||||
username,
|
||||
points,
|
||||
picture,
|
||||
updateNavHeight,
|
||||
isMapDrawerOpen,
|
||||
isMapAlreadyLoaded,
|
||||
toggleMapDrawer,
|
||||
toggleMainChat,
|
||||
shouldShowSignIn,
|
||||
params: { lang },
|
||||
trackEvent
|
||||
} = this.props;
|
||||
const navProps = {
|
||||
isOnMap: router.isActive(`/${lang}/map`),
|
||||
username,
|
||||
points,
|
||||
picture,
|
||||
updateNavHeight,
|
||||
toggleMapDrawer,
|
||||
toggleMainChat,
|
||||
shouldShowSignIn,
|
||||
trackEvent
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -70,20 +144,18 @@ export class FreeCodeCamp extends React.Component {
|
||||
<Row>
|
||||
{ this.props.children }
|
||||
</Row>
|
||||
<ToastContainer
|
||||
className='toast-bottom-right'
|
||||
ref='toaster'
|
||||
toastMessageFactory={ toastMessageFactory } />
|
||||
<MapDrawer
|
||||
isAlreadyLoaded={ isMapAlreadyLoaded }
|
||||
isOpen={ isMapDrawerOpen }
|
||||
toggleMapDrawer={ toggleMapDrawer }
|
||||
/>
|
||||
<Toasts />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const wrapComponent = compose(
|
||||
// connect Component to Redux Store
|
||||
connect(mapStateToProps, { fetchUser }),
|
||||
// handles prefetching data
|
||||
contain(fetchContainerOptions)
|
||||
);
|
||||
|
||||
export default wrapComponent(FreeCodeCamp);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
bindableActions
|
||||
)(FreeCodeCamp);
|
||||
|
49
common/app/components/Drawer.jsx
Normal file
49
common/app/components/Drawer.jsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class Drawer extends React.Component {
|
||||
static displayName = 'Drawer';
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
isOpen: PropTypes.bool,
|
||||
closeDrawer: PropTypes.func,
|
||||
closeAria: PropTypes.string,
|
||||
newTabLink: PropTypes.string,
|
||||
newTabAria: PropTypes.string
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
closeDrawer,
|
||||
closeAria,
|
||||
children,
|
||||
newTabAria,
|
||||
newTabLink
|
||||
} = this.props;
|
||||
const drawerClass = classnames({
|
||||
drawer: true,
|
||||
'is-collapsed': !isOpen
|
||||
});
|
||||
return (
|
||||
<aside className={ drawerClass }>
|
||||
<div className='drawer-action-bar'>
|
||||
<a
|
||||
aria-label={ newTabAria }
|
||||
className='drawer-action-item drawer-action-pop-out'
|
||||
href={ newTabLink }
|
||||
target='_blank'
|
||||
/>
|
||||
<button
|
||||
aria-label={ closeAria }
|
||||
className='drawer-action-item drawer-action-collapse'
|
||||
onClick={ closeDrawer }
|
||||
/>
|
||||
</div>
|
||||
<div className='drawer-content'>
|
||||
{ children }
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Col, Row, Grid } from 'react-bootstrap';
|
||||
|
||||
import links from './links.json';
|
||||
|
||||
export default class extends React.Component {
|
||||
static displayName = 'Footer';
|
||||
renderLinks(mobile) {
|
||||
return links.map(link => {
|
||||
return (
|
||||
<a
|
||||
className={ link.className}
|
||||
href={ link.href }
|
||||
key={ link.content }
|
||||
target={ link.target }>
|
||||
{ this.renderContent(mobile, link.content) }
|
||||
</a>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderContent(mobile, content) {
|
||||
if (mobile) {
|
||||
return (
|
||||
<span className='sr-only'>
|
||||
content;
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Grid className='fcc-footer'>
|
||||
<Row>
|
||||
<Col
|
||||
className='hidden-xs hidden-sm'
|
||||
xs={ 12 }>
|
||||
{ this.renderLinks() }
|
||||
</Col>
|
||||
<Col
|
||||
className='visible-xs visible-sm'
|
||||
xs={ 12 }>
|
||||
{ this.renderLinks(true) }
|
||||
</Col>
|
||||
</Row>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
Currently not used
|
@ -1 +0,0 @@
|
||||
export default from './Footer.jsx';
|
@ -1,44 +0,0 @@
|
||||
[
|
||||
{
|
||||
"className": "ion-speakerphone",
|
||||
"content": " Blog ",
|
||||
"href": "http://medium.freecodecamp.com",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"className": "ion-social-twitch-outline",
|
||||
"content": " Twitch ",
|
||||
"href": "http://www.twitch.tv/freecodecamp",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"className": "ion-social-github",
|
||||
"content": " GitHub ",
|
||||
"href": "http://github.com/freecodecamp",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"className": "ion-social-twitter",
|
||||
"content": " Twitter ",
|
||||
"href": "http://twitter.com/freecodecamp",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"className": "ion-social-facebook",
|
||||
"content": " Facebook ",
|
||||
"href": "http://facebook.com/freecodecamp",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"className": "ion-information-circled",
|
||||
"content": " About ",
|
||||
"href": "/learn-to-code",
|
||||
"target": "_self"
|
||||
},
|
||||
{
|
||||
"className": "ion-locked",
|
||||
"content": " Privacy ",
|
||||
"href": "/privacy'",
|
||||
"target": "_self"
|
||||
}
|
||||
]
|
33
common/app/components/Map-Drawer.jsx
Normal file
33
common/app/components/Map-Drawer.jsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import NoSSR from 'react-no-ssr';
|
||||
|
||||
import Drawer from './Drawer.jsx';
|
||||
import ShowMap from '../routes/challenges/components/map/Map.jsx';
|
||||
|
||||
export default class MapDrawer extends React.Component {
|
||||
static displayName = 'MapDrawer';
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool,
|
||||
isAlreadyLoaded: PropTypes.bool,
|
||||
toggleMapDrawer: PropTypes.func
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isOpen, isAlreadyLoaded, toggleMapDrawer } = this.props;
|
||||
return (
|
||||
<Drawer
|
||||
closeAria='close map aside'
|
||||
closeDrawer={ toggleMapDrawer }
|
||||
isOpen={ isOpen }
|
||||
newTabAria='open map in new tab'
|
||||
newTabLink='/map'
|
||||
>
|
||||
<NoSSR>
|
||||
<div>
|
||||
{ isAlreadyLoaded || isOpen ? <ShowMap /> : null }
|
||||
</div>
|
||||
</NoSSR>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
}
|
22
common/app/components/Nav/Avatar-Nav-Item.jsx
Normal file
22
common/app/components/Nav/Avatar-Nav-Item.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
// this is separated out to prevent react bootstrap's
|
||||
// NavBar from injecting unknown props to the li component
|
||||
export default function AvatarNavItem({ picture }) {
|
||||
return (
|
||||
<li
|
||||
className='hidden-xs hidden-sm avatar'
|
||||
key='user'
|
||||
>
|
||||
<Link to='/settings'>
|
||||
<img
|
||||
className='profile-picture float-right'
|
||||
src={ picture }
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
AvatarNavItem.propTypes = { picture: PropTypes.string };
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import {
|
||||
Col,
|
||||
@ -10,6 +11,7 @@ import {
|
||||
|
||||
import navLinks from './links.json';
|
||||
import FCCNavItem from './NavItem.jsx';
|
||||
import AvatarNavItem from './Avatar-Nav-Item.jsx';
|
||||
|
||||
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
||||
|
||||
@ -18,7 +20,8 @@ const logoElement = (
|
||||
<img
|
||||
alt='learn to code javascript at Free Code Camp logo'
|
||||
className='img-responsive nav-logo'
|
||||
src={ fCClogo } />
|
||||
src={ fCClogo }
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
||||
@ -28,26 +31,122 @@ const toggleButtonChild = (
|
||||
</Col>
|
||||
);
|
||||
|
||||
export default class extends React.Component {
|
||||
static displayName = 'Nav';
|
||||
function handleNavLinkEvent(content) {
|
||||
this.props.trackEvent({
|
||||
category: 'Nav',
|
||||
action: 'clicked',
|
||||
label: `${content} link`
|
||||
});
|
||||
}
|
||||
|
||||
export default class extends React.Component {
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
this.handleMapClickOnMap = this.handleMapClickOnMap.bind(this);
|
||||
navLinks.forEach(({ content }) => {
|
||||
this[`handle${content}Click`] = handleNavLinkEvent.bind(this, content);
|
||||
});
|
||||
}
|
||||
static displayName = 'Nav';
|
||||
static propTypes = {
|
||||
points: PropTypes.number,
|
||||
picture: PropTypes.string,
|
||||
signedIn: PropTypes.bool,
|
||||
username: PropTypes.string
|
||||
username: PropTypes.string,
|
||||
isOnMap: PropTypes.bool,
|
||||
updateNavHeight: PropTypes.func,
|
||||
toggleMapDrawer: PropTypes.func,
|
||||
toggleMainChat: PropTypes.func,
|
||||
shouldShowSignIn: PropTypes.bool,
|
||||
trackEvent: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const navBar = ReactDOM.findDOMNode(this);
|
||||
this.props.updateNavHeight(navBar.clientHeight);
|
||||
}
|
||||
|
||||
handleMapClickOnMap(e) {
|
||||
e.preventDefault();
|
||||
this.props.trackEvent({
|
||||
category: 'Nav',
|
||||
action: 'clicked',
|
||||
label: 'map clicked while on map'
|
||||
});
|
||||
}
|
||||
|
||||
handleNavClick() {
|
||||
this.props.trackEvent({
|
||||
category: 'Nav',
|
||||
action: 'clicked',
|
||||
label: 'map clicked while on map'
|
||||
});
|
||||
}
|
||||
|
||||
renderMapLink(isOnMap, toggleMapDrawer) {
|
||||
if (isOnMap) {
|
||||
return (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
href='#'
|
||||
onClick={ this.handleMapClickOnMap }
|
||||
>
|
||||
Map
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LinkContainer
|
||||
eventKey={ 1 }
|
||||
to='/map'
|
||||
>
|
||||
<NavItem
|
||||
onClick={ e => {
|
||||
if (!(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
toggleMapDrawer();
|
||||
}
|
||||
}}
|
||||
target='/map'
|
||||
>
|
||||
Map
|
||||
</NavItem>
|
||||
</LinkContainer>
|
||||
);
|
||||
}
|
||||
|
||||
renderChat(toggleMainChat) {
|
||||
return (
|
||||
<NavItem
|
||||
eventKey={ 2 }
|
||||
href='//gitter.im/freecodecamp/freecodecamp'
|
||||
onClick={ e => {
|
||||
if (!(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
toggleMainChat();
|
||||
}
|
||||
}}
|
||||
target='_blank'
|
||||
>
|
||||
Chat
|
||||
</NavItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderLinks() {
|
||||
return navLinks.map(({ content, link, react, target }, index) => {
|
||||
if (react) {
|
||||
return (
|
||||
<LinkContainer
|
||||
eventKey={ index + 1 }
|
||||
eventKey={ index + 2 }
|
||||
key={ content }
|
||||
to={ link }>
|
||||
onClick={ this[`handle${content}Click`] }
|
||||
to={ link }
|
||||
>
|
||||
<NavItem
|
||||
target={ target || null }>
|
||||
target={ target || null }
|
||||
>
|
||||
{ content }
|
||||
</NavItem>
|
||||
</LinkContainer>
|
||||
@ -58,44 +157,45 @@ export default class extends React.Component {
|
||||
eventKey={ index + 1 }
|
||||
href={ link }
|
||||
key={ content }
|
||||
target={ target || null }>
|
||||
onClick={ this[`handle${content}Click`] }
|
||||
target={ target || null }
|
||||
>
|
||||
{ content }
|
||||
</NavItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderPoints(username, points) {
|
||||
if (!username) {
|
||||
renderPoints(username, points, shouldShowSignIn) {
|
||||
if (!username || !shouldShowSignIn) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FCCNavItem
|
||||
className='brownie-points-nav'
|
||||
href={ '/' + username }>
|
||||
[ { points } ]
|
||||
</FCCNavItem>
|
||||
<LinkContainer
|
||||
eventKey={ navLinks.length + 1 }
|
||||
key='points'
|
||||
to='/settings'
|
||||
>
|
||||
<FCCNavItem className='brownie-points-nav'>
|
||||
[ { points } ]
|
||||
</FCCNavItem>
|
||||
</LinkContainer>
|
||||
);
|
||||
}
|
||||
|
||||
renderSignin(username, picture) {
|
||||
renderSignIn(username, picture, shouldShowSignIn) {
|
||||
if (!shouldShowSignIn) {
|
||||
return null;
|
||||
}
|
||||
if (username) {
|
||||
return (
|
||||
<li
|
||||
className='hidden-xs hidden-sm avatar'
|
||||
eventKey={ 2 }>
|
||||
<a href={ '/' + username }>
|
||||
<img
|
||||
className='profile-picture float-right'
|
||||
src={ picture } />
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
return <AvatarNavItem picture={ picture } />;
|
||||
} else {
|
||||
return (
|
||||
<NavItem
|
||||
eventKey={ 2 }
|
||||
href='/signin'>
|
||||
href='/signin'
|
||||
key='signin'
|
||||
>
|
||||
Sign In
|
||||
</NavItem>
|
||||
);
|
||||
@ -103,22 +203,34 @@ export default class extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { username, points, picture } = this.props;
|
||||
const {
|
||||
username,
|
||||
points,
|
||||
picture,
|
||||
isOnMap,
|
||||
toggleMapDrawer,
|
||||
toggleMainChat,
|
||||
shouldShowSignIn
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Navbar
|
||||
className='nav-height'
|
||||
fixedTop={ true }>
|
||||
fixedTop={ true }
|
||||
>
|
||||
<NavbarBrand>{ logoElement }</NavbarBrand>
|
||||
<Navbar.Toggle children={ toggleButtonChild } />
|
||||
<Navbar.Collapse eventKey={ 0 }>
|
||||
<Navbar.Collapse>
|
||||
<Nav
|
||||
className='hamburger-dropdown'
|
||||
navbar={ true }
|
||||
pullRight={ true }>
|
||||
pullRight={ true }
|
||||
>
|
||||
{ this.renderMapLink(isOnMap, toggleMapDrawer) }
|
||||
{ this.renderChat(toggleMainChat) }
|
||||
{ this.renderLinks() }
|
||||
{ this.renderPoints(username, points) }
|
||||
{ this.renderSignin(username, picture) }
|
||||
{ this.renderPoints(username, points, shouldShowSignIn) }
|
||||
{ this.renderSignIn(username, picture, shouldShowSignIn) }
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</Navbar>
|
||||
|
@ -50,8 +50,7 @@ export default React.createClass({
|
||||
target,
|
||||
children,
|
||||
'aria-controls': ariaControls, // eslint-disable-line react/prop-types
|
||||
className,
|
||||
...props
|
||||
className
|
||||
} = this.props;
|
||||
|
||||
const linkClassName = classNames(className, {
|
||||
@ -76,13 +75,14 @@ export default React.createClass({
|
||||
|
||||
return (
|
||||
<li
|
||||
{...props}
|
||||
role='presentation'>
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
{ ...linkProps }
|
||||
aria-controls={ ariaControls }
|
||||
aria-selected={ active }
|
||||
className={ linkClassName }>
|
||||
className={ linkClassName }
|
||||
>
|
||||
{ children }
|
||||
</a>
|
||||
</li>
|
||||
|
@ -1,17 +1,11 @@
|
||||
[{
|
||||
"content": "Map",
|
||||
"link": "/map"
|
||||
}, {
|
||||
"content": "Chat",
|
||||
"link": "//gitter.im/FreeCodeCamp/FreeCodeCamp",
|
||||
"target": "_blank"
|
||||
},{
|
||||
"content": "Forum",
|
||||
"link": "http://forum.freecodecamp.com/",
|
||||
"target": "_blank"
|
||||
},{
|
||||
"content": "About",
|
||||
"link": "/about"
|
||||
"link": "/about",
|
||||
"target": "_blank"
|
||||
},{
|
||||
"content": "Shop",
|
||||
"link": "/shop"
|
||||
|
@ -1,26 +1,22 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hardGoTo } from '../../redux/actions';
|
||||
|
||||
const win = typeof window !== 'undefined' ? window : {};
|
||||
|
||||
function goToServer(path) {
|
||||
win.location = '/' + path;
|
||||
}
|
||||
|
||||
export default class extends React.Component {
|
||||
export class NotFound extends React.Component {
|
||||
static displayName = 'NotFound';
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object
|
||||
location: PropTypes.object,
|
||||
hardGoTo: PropTypes.func
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
goToServer(this.props.params.splat);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.hardGoTo(this.props.location.pathname);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <span></span>;
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, { hardGoTo })(NotFound);
|
||||
|
@ -8,8 +8,8 @@ import App from './App.jsx';
|
||||
import childRoutes from './routes';
|
||||
|
||||
// redux
|
||||
import { createEpic } from 'redux-epic';
|
||||
import createReducer from './create-reducer';
|
||||
import middlewares from './middlewares';
|
||||
import sagas from './sagas';
|
||||
|
||||
// general utils
|
||||
@ -23,6 +23,7 @@ const routes = { components: App, ...childRoutes };
|
||||
// createApp(settings: {
|
||||
// location?: Location|String,
|
||||
// history?: History,
|
||||
// syncHistoryWithStore?: ((history, store) => history) = (x) => x,
|
||||
// initialState?: Object|Void,
|
||||
// serviceOptions?: Object,
|
||||
// middlewares?: Function[],
|
||||
@ -35,22 +36,30 @@ const routes = { components: App, ...childRoutes };
|
||||
export default function createApp({
|
||||
location,
|
||||
history,
|
||||
syncHistoryWithStore = (x) => x,
|
||||
syncOptions = {},
|
||||
initialState,
|
||||
serviceOptions = {},
|
||||
middlewares: sideMiddlewares = [],
|
||||
enhancers: sideEnhancers = [],
|
||||
reducers: sideReducers = {},
|
||||
sagas: sideSagas = []
|
||||
sagas: sideSagas = [],
|
||||
sagaOptions: sideSagaOptions = {}
|
||||
}) {
|
||||
const sagaOptions = {
|
||||
...sideSagaOptions,
|
||||
services: servicesCreator(null, serviceOptions)
|
||||
};
|
||||
|
||||
const sagaMiddleware = createEpic(
|
||||
sagaOptions,
|
||||
...sagas,
|
||||
...sideSagas
|
||||
);
|
||||
const enhancers = [
|
||||
applyMiddleware(
|
||||
...middlewares,
|
||||
...sideMiddlewares,
|
||||
...[ ...sagas, ...sideSagas].map(saga => saga(sagaOptions)),
|
||||
sagaMiddleware
|
||||
),
|
||||
// enhancers must come after middlewares
|
||||
// on client side these are things like Redux DevTools
|
||||
@ -63,6 +72,9 @@ export default function createApp({
|
||||
// call enhanced createStore function with reducer and initialState
|
||||
// to create store
|
||||
const store = compose(...enhancers)(createStore)(reducer, initialState);
|
||||
// sync history client side with store.
|
||||
// server side this is an identity function and history is undefined
|
||||
history = syncHistoryWithStore(history, store, syncOptions);
|
||||
|
||||
// createRouteProps({
|
||||
// redirect: LocationDescriptor,
|
||||
@ -74,6 +86,7 @@ export default function createApp({
|
||||
redirect,
|
||||
props,
|
||||
reducer,
|
||||
store
|
||||
store,
|
||||
epic: sagaMiddleware
|
||||
}));
|
||||
}
|
||||
|
@ -2,18 +2,22 @@ import { combineReducers } from 'redux';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
|
||||
import { reducer as app } from './redux';
|
||||
import { reducer as hikesApp } from './routes/Hikes/redux';
|
||||
import { reducer as toasts } from './toasts/redux';
|
||||
import entitiesReducer from './redux/entities-reducer';
|
||||
import {
|
||||
reducer as jobsApp,
|
||||
formNormalizer as jobsNormalizer
|
||||
} from './routes/Jobs/redux';
|
||||
reducer as challengesApp,
|
||||
projectNormalizer
|
||||
} from './routes/challenges/redux';
|
||||
import { reducer as settingsApp } from './routes/settings/redux';
|
||||
|
||||
export default function createReducer(sideReducers = {}) {
|
||||
return combineReducers({
|
||||
...sideReducers,
|
||||
entities: entitiesReducer,
|
||||
app,
|
||||
hikesApp,
|
||||
jobsApp,
|
||||
form: formReducer.normalize(jobsNormalizer)
|
||||
toasts,
|
||||
challengesApp,
|
||||
settingsApp,
|
||||
form: formReducer.normalize({ ...projectNormalizer })
|
||||
});
|
||||
}
|
||||
|
@ -1,32 +1,169 @@
|
||||
import { Observable } from 'rx';
|
||||
import { createAction } from 'redux-actions';
|
||||
import types from './types';
|
||||
|
||||
const throwIfUndefined = () => {
|
||||
throw new TypeError('Argument must not be of type `undefined`');
|
||||
};
|
||||
|
||||
export const createEventMeta = ({
|
||||
category = throwIfUndefined,
|
||||
action = throwIfUndefined,
|
||||
label,
|
||||
value
|
||||
} = throwIfUndefined) => ({
|
||||
analytics: {
|
||||
type: 'event',
|
||||
category,
|
||||
action,
|
||||
label,
|
||||
value
|
||||
}
|
||||
});
|
||||
|
||||
export const trackEvent = createAction(
|
||||
types.analytics,
|
||||
null,
|
||||
createEventMeta
|
||||
);
|
||||
|
||||
export const trackSocial = createAction(
|
||||
types.analytics,
|
||||
null,
|
||||
(
|
||||
network = throwIfUndefined,
|
||||
action = throwIfUndefined,
|
||||
target = throwIfUndefined
|
||||
) => ({
|
||||
analytics: {
|
||||
type: 'event',
|
||||
network,
|
||||
action,
|
||||
target
|
||||
}
|
||||
})
|
||||
);
|
||||
// updateTitle(title: String) => Action
|
||||
export const updateTitle = createAction(types.updateTitle);
|
||||
|
||||
let id = 0;
|
||||
// makeToast({ type?: String, message: String, title: String }) => Action
|
||||
export const makeToast = createAction(
|
||||
types.makeToast,
|
||||
toast => {
|
||||
id += 1;
|
||||
return {
|
||||
...toast,
|
||||
id,
|
||||
type: toast.type || 'info'
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// fetchUser() => Action
|
||||
// used in combination with fetch-user-saga
|
||||
export const fetchUser = createAction(types.fetchUser);
|
||||
|
||||
// setUser(userInfo: Object) => Action
|
||||
export const setUser = createAction(types.setUser);
|
||||
// addUser(
|
||||
// entities: { [userId]: User }
|
||||
// ) => Action
|
||||
export const addUser = createAction(
|
||||
types.addUser,
|
||||
() => {},
|
||||
entities => ({ entities })
|
||||
);
|
||||
export const updateThisUser = createAction(types.updateThisUser);
|
||||
export const showSignIn = createAction(types.showSignIn);
|
||||
|
||||
// updatePoints(points: Number) => Action
|
||||
export const updatePoints = createAction(types.updatePoints);
|
||||
// updateUserPoints(username: String, points: Number) => Action
|
||||
export const updateUserPoints = createAction(
|
||||
types.updateUserPoints,
|
||||
(username, points) => ({ username, points })
|
||||
);
|
||||
// updateUserFlag(username: String, flag: String) => Action
|
||||
export const updateUserFlag = createAction(
|
||||
types.updateUserFlag,
|
||||
(username, flag) => ({ username, flag })
|
||||
);
|
||||
// updateUserEmail(username: String, email: String) => Action
|
||||
export const updateUserEmail = createAction(
|
||||
types.updateUserFlag,
|
||||
(username, email) => ({ username, email })
|
||||
);
|
||||
// updateUserLang(username: String, lang: String) => Action
|
||||
export const updateUserLang = createAction(
|
||||
types.updateUserLang,
|
||||
(username, lang) => ({ username, lang })
|
||||
);
|
||||
export const updateAppLang = createAction(types.updateAppLang);
|
||||
// updateCompletedChallenges(username: String) => Action
|
||||
export const updateCompletedChallenges = createAction(
|
||||
types.updateCompletedChallenges
|
||||
);
|
||||
|
||||
// used when server needs client to redirect
|
||||
export const delayedRedirect = createAction(types.delayedRedirect);
|
||||
|
||||
// hardGoTo(path: String) => Action
|
||||
export const hardGoTo = createAction(types.hardGoTo);
|
||||
|
||||
export const initWindowHeight = createAction(types.initWindowHeight);
|
||||
export const updateWindowHeight = createAction(types.updateWindowHeight);
|
||||
export const updateNavHeight = createAction(types.updateNavHeight);
|
||||
|
||||
|
||||
// data
|
||||
export const updateChallengesData = createAction(types.updateChallengesData);
|
||||
export const updateJobsData = createAction(types.updateJobsData);
|
||||
export const updateHikesData = createAction(types.updateHikesData);
|
||||
|
||||
export const createErrorObservable = error => Observable.just({
|
||||
type: types.handleError,
|
||||
error
|
||||
});
|
||||
// doActionOnError(
|
||||
// actionCreator: (() => Action|Null)
|
||||
// ) => (error: Error) => Observable[Action]
|
||||
export const doActionOnError = actionCreator => error => Observable.of(
|
||||
{
|
||||
type: types.handleError,
|
||||
error
|
||||
},
|
||||
actionCreator()
|
||||
);
|
||||
|
||||
|
||||
// drawers
|
||||
export const toggleMapDrawer = createAction(
|
||||
types.toggleMapDrawer,
|
||||
null,
|
||||
() => createEventMeta({
|
||||
category: 'Nav',
|
||||
action: 'toggled',
|
||||
label: 'Map drawer toggled'
|
||||
})
|
||||
);
|
||||
export const toggleMainChat = createAction(
|
||||
types.toggleMainChat,
|
||||
null,
|
||||
() => createEventMeta({
|
||||
category: 'Nav',
|
||||
action: 'toggled',
|
||||
label: 'Main chat toggled'
|
||||
})
|
||||
);
|
||||
export const toggleHelpChat = createAction(
|
||||
types.toggleHelpChat,
|
||||
null,
|
||||
() => createEventMeta({
|
||||
category: 'Challenge',
|
||||
action: 'toggled',
|
||||
label: 'help chat toggled'
|
||||
})
|
||||
);
|
||||
export const openHelpChat = createAction(
|
||||
types.openHelpChat,
|
||||
null,
|
||||
() => createEventMeta({
|
||||
category: 'Challenge',
|
||||
action: 'opened',
|
||||
label: 'help chat opened'
|
||||
})
|
||||
);
|
||||
export const closeHelpChat = createAction(
|
||||
types.closeHelpChat,
|
||||
null,
|
||||
() => createEventMeta({
|
||||
category: 'Challenge',
|
||||
action: 'closed',
|
||||
label: 'help chat closed'
|
||||
})
|
||||
);
|
||||
|
||||
export const toggleNightMode = createAction(types.toggleNightMode);
|
||||
|
89
common/app/redux/entities-reducer.js
Normal file
89
common/app/redux/entities-reducer.js
Normal file
@ -0,0 +1,89 @@
|
||||
import types from './types';
|
||||
|
||||
const { updateUserPoints, updateCompletedChallenges } = types;
|
||||
const initialState = {
|
||||
superBlock: {},
|
||||
block: {},
|
||||
challenge: {},
|
||||
user: {}
|
||||
};
|
||||
|
||||
// future refactor(berks): Several of the actions here are just updating props
|
||||
// on the main user. These can be refactors into one response for all actions
|
||||
export default function entities(state = initialState, action) {
|
||||
const {
|
||||
type,
|
||||
payload: { email, username, points, flag, languageTag } = {}
|
||||
} = action;
|
||||
if (type === updateCompletedChallenges) {
|
||||
const username = action.payload;
|
||||
const completedChallengeMap = state.user[username].challengeMap || {};
|
||||
return {
|
||||
...state,
|
||||
challenge: Object.keys(state.challenge)
|
||||
.reduce((map, key) => {
|
||||
const challenge = state.challenge[key];
|
||||
map[key] = {
|
||||
...challenge,
|
||||
isCompleted: !!completedChallengeMap[challenge.id]
|
||||
};
|
||||
return map;
|
||||
}, {})
|
||||
};
|
||||
}
|
||||
if (action.meta && action.meta.entities) {
|
||||
return {
|
||||
...state,
|
||||
...action.meta.entities
|
||||
};
|
||||
}
|
||||
if (type === updateUserPoints) {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[username]: {
|
||||
...state.user[username],
|
||||
points
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if (action.type === types.updateUserFlag) {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[username]: {
|
||||
...state.user[username],
|
||||
[flag]: !state.user[username][flag]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if (action.type === types.updateUserEmail) {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[username]: {
|
||||
...state.user[username],
|
||||
email
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if (action.type === types.updateUserLang) {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[username]: {
|
||||
...state.user[username],
|
||||
languageTag
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
@ -1,39 +1,30 @@
|
||||
import { Observable } from 'rx';
|
||||
import { handleError, setUser, fetchUser } from './types';
|
||||
import types from './types';
|
||||
import {
|
||||
addUser,
|
||||
updateThisUser,
|
||||
updateCompletedChallenges,
|
||||
createErrorObservable,
|
||||
showSignIn
|
||||
} from './actions';
|
||||
|
||||
export default ({ services }) => ({ dispatch }) => next => {
|
||||
return function getUserSaga(action) {
|
||||
if (action.type !== fetchUser) {
|
||||
return next(action);
|
||||
}
|
||||
const { fetchUser } = types;
|
||||
|
||||
return services.readService$({ service: 'user' })
|
||||
.map(({
|
||||
username,
|
||||
picture,
|
||||
points,
|
||||
isFrontEndCert,
|
||||
isBackEndCert,
|
||||
isFullStackCert
|
||||
}) => {
|
||||
return {
|
||||
type: setUser,
|
||||
payload: {
|
||||
username,
|
||||
picture,
|
||||
points,
|
||||
isFrontEndCert,
|
||||
isBackEndCert,
|
||||
isFullStackCert,
|
||||
isSignedIn: true
|
||||
export default function getUserSaga(action$, getState, { services }) {
|
||||
return action$
|
||||
.filter(action => action.type === fetchUser)
|
||||
.flatMap(() => {
|
||||
return services.readService$({ service: 'user' })
|
||||
.flatMap(({ entities, result })=> {
|
||||
if (!entities || !result) {
|
||||
return Observable.just(showSignIn());
|
||||
}
|
||||
};
|
||||
})
|
||||
.catch(error => Observable.just({
|
||||
type: handleError,
|
||||
error
|
||||
}))
|
||||
.doOnNext(dispatch);
|
||||
};
|
||||
};
|
||||
|
||||
return Observable.of(
|
||||
addUser(entities),
|
||||
updateThisUser(result),
|
||||
updateCompletedChallenges(result)
|
||||
);
|
||||
})
|
||||
.catch(createErrorObservable);
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
export { default as reducer } from './reducer';
|
||||
export { default as actions } from './actions';
|
||||
export { default as types } from './types';
|
||||
|
||||
import fetchUserSaga from './fetch-user-saga';
|
||||
|
||||
export { default as reducer } from './reducer';
|
||||
export * as actions from './actions';
|
||||
export { default as types } from './types';
|
||||
export const sagas = [ fetchUserSaga ];
|
||||
|
@ -1,6 +1,18 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
import types from './types';
|
||||
|
||||
const initialState = {
|
||||
title: 'Learn To Code | Free Code Camp',
|
||||
shouldShowSignIn: false,
|
||||
user: '',
|
||||
lang: '',
|
||||
csrfToken: '',
|
||||
windowHeight: 0,
|
||||
navHeight: 0,
|
||||
isMainChatOpen: false,
|
||||
isHelpChatOpen: false
|
||||
};
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
|
||||
@ -8,29 +20,57 @@ export default handleActions(
|
||||
title: payload + ' | Free Code Camp'
|
||||
}),
|
||||
|
||||
[types.makeToast]: (state, { payload: toast }) => ({
|
||||
[types.updateThisUser]: (state, { payload: user }) => ({
|
||||
...state,
|
||||
toast
|
||||
user,
|
||||
shouldShowSignIn: true
|
||||
}),
|
||||
[types.updateAppLang]: (state, { payload = 'en' }) =>({
|
||||
...state,
|
||||
lang: payload
|
||||
}),
|
||||
[types.showSignIn]: state => ({
|
||||
...state,
|
||||
shouldShowSignIn: true
|
||||
}),
|
||||
|
||||
[types.setUser]: (state, { payload: user }) => ({ ...state, ...user }),
|
||||
|
||||
[types.challengeSaved]: (state, { payload: { points = 0 } }) => ({
|
||||
...state,
|
||||
points
|
||||
}),
|
||||
|
||||
[types.updatePoints]: (state, { payload: points }) => ({
|
||||
[types.updateWindowHeight]: (state, { payload: windowHeight }) => ({
|
||||
...state,
|
||||
points
|
||||
windowHeight
|
||||
}),
|
||||
[types.updateNavHeight]: (state, { payload: navHeight }) => ({
|
||||
...state,
|
||||
navHeight
|
||||
}),
|
||||
[types.toggleMapDrawer]: state => ({
|
||||
...state,
|
||||
isMapAlreadyLoaded: true,
|
||||
isMapDrawerOpen: !state.isMapDrawerOpen
|
||||
}),
|
||||
[types.toggleMainChat]: state => ({
|
||||
...state,
|
||||
isMainChatOpen: !state.isMainChatOpen
|
||||
}),
|
||||
[types.toggleHelpChat]: state => ({
|
||||
...state,
|
||||
isHelpChatOpen: !state.isHelpChatOpen
|
||||
}),
|
||||
[types.openHelpChat]: state => ({
|
||||
...state,
|
||||
isHelpChatOpen: true
|
||||
}),
|
||||
[types.closeHelpChat]: state => ({
|
||||
...state,
|
||||
isHelpChatOpen: false
|
||||
}),
|
||||
[types.delayedRedirect]: (state, { payload }) => ({
|
||||
...state,
|
||||
delayedRedirect: payload
|
||||
})
|
||||
},
|
||||
{
|
||||
title: 'Learn To Code | Free Code Camp',
|
||||
username: null,
|
||||
picture: null,
|
||||
points: 0,
|
||||
isSignedIn: false,
|
||||
csrfToken: ''
|
||||
}
|
||||
initialState
|
||||
);
|
||||
|
9
common/app/redux/selectors.js
Normal file
9
common/app/redux/selectors.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
export const userSelector = createSelector(
|
||||
state => state.app.user,
|
||||
state => state.entities.user,
|
||||
(username, userMap) => ({
|
||||
user: userMap[username] || {}
|
||||
})
|
||||
);
|
@ -1,14 +1,45 @@
|
||||
import createTypes from '../utils/create-types';
|
||||
|
||||
export default createTypes([
|
||||
'analytics',
|
||||
'updateTitle',
|
||||
'updateAppLang',
|
||||
|
||||
'fetchUser',
|
||||
'setUser',
|
||||
'addUser',
|
||||
'updateThisUser',
|
||||
'updateUserPoints',
|
||||
'updateUserFlag',
|
||||
'updateUserEmail',
|
||||
'updateUserLang',
|
||||
'updateCompletedChallenges',
|
||||
'showSignIn',
|
||||
|
||||
'makeToast',
|
||||
'updatePoints',
|
||||
'handleError',
|
||||
'toggleNightMode',
|
||||
// used to hit the server
|
||||
'hardGoTo'
|
||||
'hardGoTo',
|
||||
'delayedRedirect',
|
||||
|
||||
'initWindowHeight',
|
||||
'updateWindowHeight',
|
||||
'updateNavHeight',
|
||||
|
||||
// data handling
|
||||
'updateChallengesData',
|
||||
'updateJobsData',
|
||||
'updateHikesData',
|
||||
|
||||
// drawers
|
||||
'toggleMapDrawer',
|
||||
'toggleWikiDrawer',
|
||||
|
||||
// chat
|
||||
'openMainChat',
|
||||
'closeMainChat',
|
||||
'toggleMainChat',
|
||||
|
||||
'openHelpChat',
|
||||
'closeHelpChat',
|
||||
'toggleHelpChat'
|
||||
], 'app');
|
||||
|
@ -1 +0,0 @@
|
||||
This folder contains things relative to the bonfires' screens
|
@ -1,71 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { createSelector } from 'reselect';
|
||||
// import debug from 'debug';
|
||||
|
||||
import HikesMap from './Map.jsx';
|
||||
import { fetchHikes } from '../redux/actions';
|
||||
|
||||
import contain from '../../../utils/professor-x';
|
||||
|
||||
// const log = debug('fcc:hikes');
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
state => state.hikesApp.hikes.entities,
|
||||
state => state.hikesApp.hikes.results,
|
||||
(hikesMap, hikesByDashedName)=> {
|
||||
if (!hikesMap || !hikesByDashedName) {
|
||||
return { hikes: [] };
|
||||
}
|
||||
return {
|
||||
hikes: hikesByDashedName.map(dashedName => hikesMap[dashedName])
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const fetchOptions = {
|
||||
fetchAction: 'fetchHikes',
|
||||
isPrimed: ({ hikes }) => hikes && !!hikes.length,
|
||||
getActionArgs: ({ params: { dashedName } }) => [ dashedName ],
|
||||
shouldContainerFetch(props, nextProps) {
|
||||
return props.params.dashedName !== nextProps.params.dashedName;
|
||||
}
|
||||
};
|
||||
|
||||
export class Hikes extends PureComponent {
|
||||
static displayName = 'Hikes';
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.element,
|
||||
hikes: PropTypes.array,
|
||||
params: PropTypes.object
|
||||
};
|
||||
|
||||
renderMap(hikes) {
|
||||
return (
|
||||
<HikesMap hikes={ hikes }/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hikes } = this.props;
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
// render sub-route
|
||||
this.props.children ||
|
||||
// if no sub-route render hikes map
|
||||
this.renderMap(hikes)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// export redux and fetch aware component
|
||||
export default compose(
|
||||
connect(mapStateToProps, { fetchHikes }),
|
||||
contain(fetchOptions)
|
||||
)(Hikes);
|
@ -1,102 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Col, Row } from 'react-bootstrap';
|
||||
import Youtube from 'react-youtube';
|
||||
import { createSelector } from 'reselect';
|
||||
import debug from 'debug';
|
||||
|
||||
import { hardGoTo } from '../../../redux/actions';
|
||||
import { toggleQuestionView } from '../redux/actions';
|
||||
import { getCurrentHike } from '../redux/selectors';
|
||||
|
||||
const log = debug('fcc:hikes');
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
getCurrentHike,
|
||||
(currentHike) => {
|
||||
const {
|
||||
dashedName,
|
||||
description,
|
||||
challengeSeed: [id] = [0]
|
||||
} = currentHike;
|
||||
return {
|
||||
id,
|
||||
dashedName,
|
||||
description
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export class Lecture extends React.Component {
|
||||
static displayName = 'Lecture';
|
||||
|
||||
static propTypes = {
|
||||
// actions
|
||||
toggleQuestionView: PropTypes.func,
|
||||
// ui
|
||||
id: PropTypes.string,
|
||||
description: PropTypes.array,
|
||||
dashedName: PropTypes.string,
|
||||
hardGoTo: PropTypes.func
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.props.id) {
|
||||
this.props.hardGoTo('/map');
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { props } = this;
|
||||
return nextProps.id !== props.id;
|
||||
}
|
||||
|
||||
handleError: log;
|
||||
|
||||
renderTranscript(transcript, dashedName) {
|
||||
return transcript.map((line, index) => (
|
||||
<p
|
||||
className='lead text-left'
|
||||
dangerouslySetInnerHTML={{__html: line}}
|
||||
key={ dashedName + index } />
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
id = '1',
|
||||
description = [],
|
||||
toggleQuestionView
|
||||
} = this.props;
|
||||
|
||||
const dashedName = 'foo';
|
||||
|
||||
return (
|
||||
<Col xs={ 12 }>
|
||||
<Row>
|
||||
<Youtube
|
||||
id='player_1'
|
||||
onError={ this.handleError }
|
||||
videoId={ id } />
|
||||
</Row>
|
||||
<Row>
|
||||
<article>
|
||||
{ this.renderTranscript(description, dashedName) }
|
||||
</article>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
onClick={ toggleQuestionView }>
|
||||
Take me to the Questions
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ hardGoTo, toggleQuestionView }
|
||||
)(Lecture);
|
@ -1,39 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'HikesMap',
|
||||
|
||||
propTypes: {
|
||||
hikes: PropTypes.array
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
hikes = [{}]
|
||||
} = this.props;
|
||||
|
||||
const vidElements = hikes.map(({ title, dashedName }) => {
|
||||
return (
|
||||
<ListGroupItem key={ dashedName }>
|
||||
<Link to={ `/videos/${dashedName}` }>
|
||||
<h3>{ title }</h3>
|
||||
</Link>
|
||||
</ListGroupItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='text-center'>
|
||||
<h2>Welcome To Hikes!</h2>
|
||||
</div>
|
||||
<hr />
|
||||
<ListGroup>
|
||||
{ vidElements }
|
||||
</ListGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
import Hikes from './components/Hikes.jsx';
|
||||
import Hike from './components/Hike.jsx';
|
||||
|
||||
export default {
|
||||
path: 'videos',
|
||||
component: Hikes,
|
||||
childRoutes: [{
|
||||
path: ':dashedName',
|
||||
component: Hike
|
||||
}]
|
||||
};
|
@ -1,57 +0,0 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
|
||||
import types from './types';
|
||||
import { getMouse } from './utils';
|
||||
|
||||
|
||||
// fetchHikes(dashedName?: String) => Action
|
||||
// used with fetchHikesSaga
|
||||
export const fetchHikes = createAction(types.fetchHikes);
|
||||
|
||||
// fetchHikesCompleted(hikes: Object) => Action
|
||||
// hikes is a normalized response from server
|
||||
// called within fetchHikesSaga
|
||||
export const fetchHikesCompleted = createAction(
|
||||
types.fetchHikesCompleted,
|
||||
(hikes, currentHike) => ({ hikes, currentHike })
|
||||
);
|
||||
|
||||
export const resetHike = createAction(types.resetHike);
|
||||
|
||||
export const toggleQuestionView = createAction(types.toggleQuestionView);
|
||||
|
||||
export const grabQuestion = createAction(types.grabQuestion, e => {
|
||||
let { pageX, pageY, touches } = e;
|
||||
if (touches) {
|
||||
e.preventDefault();
|
||||
// these re-assigns the values of pageX, pageY from touches
|
||||
({ pageX, pageY } = touches[0]);
|
||||
}
|
||||
const delta = [pageX, pageY];
|
||||
const mouse = [0, 0];
|
||||
|
||||
return { delta, mouse };
|
||||
});
|
||||
|
||||
export const releaseQuestion = createAction(types.releaseQuestion);
|
||||
export const moveQuestion = createAction(
|
||||
types.moveQuestion,
|
||||
({ e, delta }) => getMouse(e, delta)
|
||||
);
|
||||
|
||||
// answer({
|
||||
// e: Event,
|
||||
// answer: Boolean,
|
||||
// userAnswer: Boolean,
|
||||
// info: String,
|
||||
// threshold: Number
|
||||
// }) => Action
|
||||
export const answerQuestion = createAction(types.answerQuestion);
|
||||
|
||||
export const startShake = createAction(types.startShake);
|
||||
export const endShake = createAction(types.primeNextQuestion);
|
||||
|
||||
export const goToNextQuestion = createAction(types.goToNextQuestion);
|
||||
|
||||
export const hikeCompleted = createAction(types.hikeCompleted);
|
||||
export const goToNextHike = createAction(types.goToNextHike);
|
@ -1,146 +0,0 @@
|
||||
import { Observable } from 'rx';
|
||||
import { push } from 'react-router-redux';
|
||||
|
||||
import types from './types';
|
||||
import { getMouse } from './utils';
|
||||
|
||||
import { makeToast, updatePoints } from '../../../redux/actions';
|
||||
import { hikeCompleted, goToNextHike } from './actions';
|
||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||
import { getCurrentHike } from './selectors';
|
||||
|
||||
function handleAnswer(getState, dispatch, next, action) {
|
||||
const {
|
||||
e,
|
||||
answer,
|
||||
userAnswer,
|
||||
info,
|
||||
threshold
|
||||
} = action.payload;
|
||||
|
||||
const state = getState();
|
||||
const { id, name, challengeType, tests } = getCurrentHike(state);
|
||||
const {
|
||||
app: { isSignedIn, csrfToken },
|
||||
hikesApp: {
|
||||
currentQuestion,
|
||||
delta = [ 0, 0 ]
|
||||
}
|
||||
} = state;
|
||||
|
||||
let finalAnswer;
|
||||
// drag answer, compute response
|
||||
if (typeof userAnswer === 'undefined') {
|
||||
const [positionX] = getMouse(e, delta);
|
||||
|
||||
// question released under threshold
|
||||
if (Math.abs(positionX) < threshold) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
if (positionX >= threshold) {
|
||||
finalAnswer = true;
|
||||
}
|
||||
|
||||
if (positionX <= -threshold) {
|
||||
finalAnswer = false;
|
||||
}
|
||||
} else {
|
||||
finalAnswer = userAnswer;
|
||||
}
|
||||
|
||||
// incorrect question
|
||||
if (answer !== finalAnswer) {
|
||||
if (info) {
|
||||
dispatch(makeToast({
|
||||
title: 'Hint',
|
||||
message: info,
|
||||
type: 'info'
|
||||
}));
|
||||
}
|
||||
|
||||
return Observable
|
||||
.just({ type: types.endShake })
|
||||
.delay(500)
|
||||
.startWith({ type: types.startShake })
|
||||
.doOnNext(dispatch);
|
||||
}
|
||||
|
||||
if (tests[currentQuestion]) {
|
||||
return Observable
|
||||
.just({ type: types.goToNextQuestion })
|
||||
.delay(300)
|
||||
.startWith({ type: types.primeNextQuestion })
|
||||
.doOnNext(dispatch);
|
||||
}
|
||||
|
||||
let updateUser$;
|
||||
if (isSignedIn) {
|
||||
const body = { id, name, challengeType: +challengeType, _csrf: csrfToken };
|
||||
updateUser$ = postJSON$('/completed-challenge', body)
|
||||
// if post fails, will retry once
|
||||
.retry(3)
|
||||
.flatMap(({ alreadyCompleted, points }) => {
|
||||
return Observable.of(
|
||||
makeToast({
|
||||
message:
|
||||
'Challenge saved.' +
|
||||
(alreadyCompleted ? '' : ' First time Completed!'),
|
||||
title: 'Saved',
|
||||
type: 'info'
|
||||
}),
|
||||
updatePoints(points),
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
return Observable.just({
|
||||
type: 'app.error',
|
||||
error
|
||||
});
|
||||
});
|
||||
} else {
|
||||
updateUser$ = Observable.empty();
|
||||
}
|
||||
|
||||
const challengeCompleted$ = Observable.of(
|
||||
goToNextHike(),
|
||||
makeToast({
|
||||
title: 'Congratulations!',
|
||||
message: 'Hike completed.' + (isSignedIn ? ' Saving...' : ''),
|
||||
type: 'success'
|
||||
})
|
||||
);
|
||||
|
||||
return Observable.merge(challengeCompleted$, updateUser$)
|
||||
.delay(300)
|
||||
.startWith(hikeCompleted(finalAnswer))
|
||||
.catch(error => Observable.just({
|
||||
type: 'error',
|
||||
error
|
||||
}))
|
||||
// end with action so we know it is ok to transition
|
||||
.doOnCompleted(() => dispatch({ type: types.transitionHike }))
|
||||
.doOnNext(dispatch);
|
||||
}
|
||||
|
||||
export default () => ({ getState, dispatch }) => next => {
|
||||
return function answerSaga(action) {
|
||||
if (action.type === types.answerQuestion) {
|
||||
return handleAnswer(getState, dispatch, next, action);
|
||||
}
|
||||
|
||||
// let goToNextQuestion hit reducers first
|
||||
const result = next(action);
|
||||
if (action.type === types.transitionHike) {
|
||||
const { hikesApp: { currentHike } } = getState();
|
||||
// if no next hike currentHike will equal '' which is falsy
|
||||
if (currentHike) {
|
||||
dispatch(push(`/videos/${currentHike}`));
|
||||
} else {
|
||||
dispatch(push('/map'));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
import { Observable } from 'rx';
|
||||
import { normalize, Schema, arrayOf } from 'normalizr';
|
||||
// import debug from 'debug';
|
||||
|
||||
import types from './types';
|
||||
import { fetchHikesCompleted } from './actions';
|
||||
import { handleError } from '../../../redux/types';
|
||||
|
||||
import { findCurrentHike } from './utils';
|
||||
|
||||
// const log = debug('fcc:fetch-hikes-saga');
|
||||
const hike = new Schema('hike', { idAttribute: 'dashedName' });
|
||||
|
||||
export default ({ services }) => ({ dispatch }) => next => {
|
||||
return function fetchHikesSaga(action) {
|
||||
if (action.type !== types.fetchHikes) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const dashedName = action.payload;
|
||||
return services.readService$({ service: 'hikes' })
|
||||
.map(hikes => {
|
||||
const { entities, result } = normalize(
|
||||
{ hikes },
|
||||
{ hikes: arrayOf(hike) }
|
||||
);
|
||||
|
||||
hikes = {
|
||||
entities: entities.hike,
|
||||
results: result.hikes
|
||||
};
|
||||
|
||||
const currentHike = findCurrentHike(hikes, dashedName);
|
||||
|
||||
return fetchHikesCompleted(hikes, currentHike);
|
||||
})
|
||||
.catch(error => {
|
||||
return Observable.just({
|
||||
type: handleError,
|
||||
error
|
||||
});
|
||||
})
|
||||
.doOnNext(dispatch);
|
||||
};
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
export actions from './actions';
|
||||
export reducer from './reducer';
|
||||
export types from './types';
|
||||
|
||||
import answerSaga from './answer-saga';
|
||||
import fetchHikesSaga from './fetch-hikes-saga';
|
||||
|
||||
export const sagas = [ answerSaga, fetchHikesSaga ];
|
@ -1,103 +0,0 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
import types from './types';
|
||||
import { findNextHikeName } from './utils';
|
||||
|
||||
const initialState = {
|
||||
hikes: {
|
||||
results: [],
|
||||
entities: {}
|
||||
},
|
||||
// ui
|
||||
// hike dashedName
|
||||
currentHike: '',
|
||||
// 1 indexed
|
||||
currentQuestion: 1,
|
||||
// [ xPosition, yPosition ]
|
||||
mouse: [ 0, 0 ],
|
||||
// change in mouse position since pressed
|
||||
// [ xDelta, yDelta ]
|
||||
delta: [ 0, 0 ],
|
||||
isPressed: false,
|
||||
isCorrect: false,
|
||||
shouldShakeQuestion: false,
|
||||
shouldShowQuestions: false
|
||||
};
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[types.toggleQuestionView]: state => ({
|
||||
...state,
|
||||
shouldShowQuestions: !state.shouldShowQuestions,
|
||||
currentQuestion: 1
|
||||
}),
|
||||
|
||||
[types.grabQuestion]: (state, { payload: { delta, mouse } }) => ({
|
||||
...state,
|
||||
isPressed: true,
|
||||
delta,
|
||||
mouse
|
||||
}),
|
||||
|
||||
[types.releaseQuestion]: state => ({
|
||||
...state,
|
||||
isPressed: false,
|
||||
mouse: [ 0, 0 ]
|
||||
}),
|
||||
|
||||
[types.moveQuestion]: (state, { payload: mouse }) => ({ ...state, mouse }),
|
||||
|
||||
[types.resetHike]: state => ({
|
||||
...state,
|
||||
currentQuestion: 1,
|
||||
shouldShowQuestions: false,
|
||||
mouse: [0, 0],
|
||||
delta: [0, 0]
|
||||
}),
|
||||
|
||||
[types.startShake]: state => ({ ...state, shouldShakeQuestion: true }),
|
||||
[types.endShake]: state => ({ ...state, shouldShakeQuestion: false }),
|
||||
|
||||
[types.primeNextQuestion]: (state, { payload: userAnswer }) => ({
|
||||
...state,
|
||||
currentQuestion: state.currentQuestion + 1,
|
||||
mouse: [ userAnswer ? 1000 : -1000, 0],
|
||||
isPressed: false
|
||||
}),
|
||||
|
||||
[types.goToNextQuestion]: state => ({
|
||||
...state,
|
||||
mouse: [ 0, 0 ]
|
||||
}),
|
||||
|
||||
[types.hikeCompleted]: (state, { payload: userAnswer } ) => ({
|
||||
...state,
|
||||
isCorrect: true,
|
||||
isPressed: false,
|
||||
delta: [ 0, 0 ],
|
||||
mouse: [ userAnswer ? 1000 : -1000, 0]
|
||||
}),
|
||||
|
||||
[types.goToNextHike]: state => ({
|
||||
...state,
|
||||
currentHike: findNextHikeName(state.hikes, state.currentHike),
|
||||
mouse: [ 0, 0 ]
|
||||
}),
|
||||
|
||||
[types.transitionHike]: state => ({
|
||||
...state,
|
||||
showQuestions: false,
|
||||
currentQuestion: 1
|
||||
}),
|
||||
|
||||
[types.fetchHikesCompleted]: (state, { payload }) => {
|
||||
const { hikes, currentHike } = payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
hikes,
|
||||
currentHike
|
||||
};
|
||||
}
|
||||
},
|
||||
initialState
|
||||
);
|
@ -1,8 +0,0 @@
|
||||
// use this file for common selectors
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
export const getCurrentHike = createSelector(
|
||||
state => state.hikesApp.hikes.entities,
|
||||
state => state.hikesApp.currentHike,
|
||||
(hikesMap, currentHikeDashedName) => (hikesMap[currentHikeDashedName] || {})
|
||||
);
|
@ -1,24 +0,0 @@
|
||||
import createTypes from '../../../utils/create-types';
|
||||
|
||||
export default createTypes([
|
||||
'fetchHikes',
|
||||
'fetchHikesCompleted',
|
||||
'resetHike',
|
||||
|
||||
'toggleQuestionView',
|
||||
'grabQuestion',
|
||||
'releaseQuestion',
|
||||
'moveQuestion',
|
||||
|
||||
'answerQuestion',
|
||||
|
||||
'startShake',
|
||||
'endShake',
|
||||
|
||||
'primeNextQuestion',
|
||||
'goToNextQuestion',
|
||||
'transitionHike',
|
||||
|
||||
'hikeCompleted',
|
||||
'goToNextHike'
|
||||
], 'videos');
|
@ -1,77 +0,0 @@
|
||||
import debug from 'debug';
|
||||
import _ from 'lodash';
|
||||
|
||||
const log = debug('fcc:hikes:utils');
|
||||
|
||||
function getFirstHike(hikes) {
|
||||
return hikes.results[0];
|
||||
}
|
||||
|
||||
// interface Hikes {
|
||||
// results: String[],
|
||||
// entities: {
|
||||
// hikeId: Challenge
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// findCurrentHike({
|
||||
// hikes: Hikes,
|
||||
// dashedName: String
|
||||
// }) => String
|
||||
export function findCurrentHike(hikes, dashedName) {
|
||||
if (!dashedName) {
|
||||
return getFirstHike(hikes) || {};
|
||||
}
|
||||
|
||||
const filterRegex = new RegExp(dashedName, 'i');
|
||||
|
||||
return hikes
|
||||
.results
|
||||
.filter(dashedName => {
|
||||
return filterRegex.test(dashedName);
|
||||
})
|
||||
.reduce((throwAway, hike) => {
|
||||
return hike;
|
||||
}, '');
|
||||
}
|
||||
|
||||
export function getCurrentHike(hikes = {}, dashedName) {
|
||||
if (!dashedName) {
|
||||
return getFirstHike(hikes) || {};
|
||||
}
|
||||
return hikes.entities[dashedName];
|
||||
}
|
||||
|
||||
// findNextHikeName(
|
||||
// hikes: { results: String[] },
|
||||
// dashedName: String
|
||||
// ) => String
|
||||
export function findNextHikeName({ results }, dashedName) {
|
||||
if (!dashedName) {
|
||||
log('find next hike no id provided');
|
||||
return results[0];
|
||||
}
|
||||
const currentIndex = _.findIndex(
|
||||
results,
|
||||
_dashedName => _dashedName === dashedName
|
||||
);
|
||||
|
||||
if (currentIndex >= results.length) {
|
||||
return '';
|
||||
}
|
||||
return results[currentIndex + 1];
|
||||
}
|
||||
|
||||
|
||||
export function getMouse(e, [dx, dy]) {
|
||||
let { pageX, pageY, touches, changedTouches } = e;
|
||||
|
||||
// touches can be empty on touchend
|
||||
if (touches || changedTouches) {
|
||||
e.preventDefault();
|
||||
// these re-assigns the values of pageX, pageY from touches
|
||||
({ pageX, pageY } = touches[0] || changedTouches[0]);
|
||||
}
|
||||
|
||||
return [pageX - dx, pageY - dy];
|
||||
}
|
@ -1 +0,0 @@
|
||||
This folder contains everything relative to Jobs board
|
@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import { Button, Row, Col } from 'react-bootstrap';
|
||||
|
||||
export default class extends React.Component {
|
||||
static displayName = 'NoJobFound';
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<Col
|
||||
md={ 6 }
|
||||
mdOffset={ 3 }>
|
||||
<div>
|
||||
No job found...
|
||||
<LinkContainer to='/jobs'>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='large'
|
||||
bsStyle='primary'>
|
||||
Go to the job board
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,306 +0,0 @@
|
||||
import { CompositeDisposable } from 'rx';
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Button, Input, Col, Row, Well } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { push } from 'react-router-redux';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import {
|
||||
applyPromo,
|
||||
clearPromo,
|
||||
updatePromo
|
||||
} from '../redux/actions';
|
||||
|
||||
// real paypal buttons
|
||||
// will take your money
|
||||
const paypalIds = {
|
||||
regular: 'Q8Z82ZLAX3Q8N',
|
||||
highlighted: 'VC8QPSKCYMZLN'
|
||||
};
|
||||
|
||||
const bindableActions = {
|
||||
applyPromo,
|
||||
clearPromo,
|
||||
push,
|
||||
updatePromo
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
state => state.jobsApp.newJob,
|
||||
state => state.jobsApp,
|
||||
(
|
||||
{ id, isHighlighted } = {},
|
||||
{
|
||||
buttonId,
|
||||
price = 1000,
|
||||
discountAmount = 0,
|
||||
promoCode = '',
|
||||
promoApplied = false,
|
||||
promoName = ''
|
||||
}
|
||||
) => {
|
||||
if (!buttonId) {
|
||||
buttonId = isHighlighted ?
|
||||
paypalIds.highlighted :
|
||||
paypalIds.regular;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
isHighlighted,
|
||||
price,
|
||||
discountAmount,
|
||||
promoName,
|
||||
promoCode,
|
||||
promoApplied
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export class JobTotal extends PureComponent {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this._subscriptions = new CompositeDisposable();
|
||||
}
|
||||
|
||||
static displayName = 'JobTotal';
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string,
|
||||
isHighlighted: PropTypes.bool,
|
||||
buttonId: PropTypes.string,
|
||||
price: PropTypes.number,
|
||||
discountAmount: PropTypes.number,
|
||||
promoName: PropTypes.string,
|
||||
promoCode: PropTypes.string,
|
||||
promoApplied: PropTypes.bool
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.props.id) {
|
||||
this.props.push('/jobs');
|
||||
}
|
||||
|
||||
this.props.clearPromo();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._subscriptions.dispose();
|
||||
}
|
||||
|
||||
renderDiscount(discountAmount) {
|
||||
if (!discountAmount) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Row>
|
||||
<Col
|
||||
md={ 3 }
|
||||
mdOffset={ 3 }>
|
||||
<h4>Promo Discount</h4>
|
||||
</Col>
|
||||
<Col
|
||||
md={ 3 }>
|
||||
<h4>-{ discountAmount }</h4>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
renderHighlightPrice(isHighlighted) {
|
||||
if (!isHighlighted) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Row>
|
||||
<Col
|
||||
md={ 3 }
|
||||
mdOffset={ 3 }>
|
||||
<h4>Highlighting</h4>
|
||||
</Col>
|
||||
<Col
|
||||
md={ 3 }>
|
||||
<h4>+ 250</h4>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
renderPromo() {
|
||||
const {
|
||||
id,
|
||||
promoApplied,
|
||||
promoCode,
|
||||
promoName,
|
||||
isHighlighted,
|
||||
applyPromo,
|
||||
updatePromo
|
||||
} = this.props;
|
||||
|
||||
if (promoApplied) {
|
||||
return (
|
||||
<div>
|
||||
<div className='spacer' />
|
||||
<Row>
|
||||
<Col
|
||||
md={ 3 }
|
||||
mdOffset={ 3 }>
|
||||
{ promoName } applied
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='spacer' />
|
||||
<Row>
|
||||
<Col
|
||||
md={ 3 }
|
||||
mdOffset={ 3 }>
|
||||
Have a promo code?
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col
|
||||
md={ 3 }
|
||||
mdOffset={ 3 }>
|
||||
<Input
|
||||
onChange={ updatePromo }
|
||||
type='text'
|
||||
value={ promoCode } />
|
||||
</Col>
|
||||
<Col
|
||||
md={ 3 }>
|
||||
<Button
|
||||
block={ true }
|
||||
onClick={ () => {
|
||||
const subscription = applyPromo({
|
||||
id,
|
||||
code: promoCode,
|
||||
type: isHighlighted ? 'isHighlighted' : null
|
||||
}).subscribe();
|
||||
this._subscriptions.add(subscription);
|
||||
}}>
|
||||
Apply Promo Code
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
isHighlighted,
|
||||
buttonId,
|
||||
price,
|
||||
discountAmount,
|
||||
push
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<Col
|
||||
md={ 10 }
|
||||
mdOffset={ 1 }
|
||||
sm={ 8 }
|
||||
smOffset={ 2 }
|
||||
xs={ 12 }>
|
||||
<div>
|
||||
<Row>
|
||||
<Col
|
||||
md={ 6 }
|
||||
mdOffset={ 3 }>
|
||||
<h2 className='text-center'>
|
||||
One more step
|
||||
</h2>
|
||||
<div className='spacer' />
|
||||
You're Awesome! just one more step to go.
|
||||
Clicking on the link below will redirect to paypal.
|
||||
</Col>
|
||||
</Row>
|
||||
<div className='spacer' />
|
||||
<Well>
|
||||
<Row>
|
||||
<Col
|
||||
md={ 3 }
|
||||
mdOffset={ 3 }>
|
||||
<h4>Job Posting</h4>
|
||||
</Col>
|
||||
<Col
|
||||
md={ 6 }>
|
||||
<h4>+ { price }</h4>
|
||||
</Col>
|
||||
</Row>
|
||||
{ this.renderHighlightPrice(isHighlighted) }
|
||||
{ this.renderDiscount(discountAmount) }
|
||||
<Row>
|
||||
<Col
|
||||
md={ 3 }
|
||||
mdOffset={ 3 }>
|
||||
<h4>Total</h4>
|
||||
</Col>
|
||||
<Col
|
||||
md={ 6 }>
|
||||
<h4>${
|
||||
price - discountAmount + (isHighlighted ? 250 : 0)
|
||||
}</h4>
|
||||
</Col>
|
||||
</Row>
|
||||
</Well>
|
||||
{ this.renderPromo() }
|
||||
<div className='spacer' />
|
||||
<Row>
|
||||
<Col
|
||||
md={ 6 }
|
||||
mdOffset={ 3 }>
|
||||
<form
|
||||
action='https://www.paypal.com/cgi-bin/webscr'
|
||||
method='post'
|
||||
onClick={ () => setTimeout(push, 0, '/jobs') }
|
||||
target='_blank'>
|
||||
<input
|
||||
name='cmd'
|
||||
type='hidden'
|
||||
value='_s-xclick' />
|
||||
<input
|
||||
name='hosted_button_id'
|
||||
type='hidden'
|
||||
value={ buttonId } />
|
||||
<input
|
||||
name='custom'
|
||||
type='hidden'
|
||||
value={ '' + id } />
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='large'
|
||||
className='signup-btn'
|
||||
type='submit'>
|
||||
<i className='fa fa-paypal' />
|
||||
Continue to PayPal
|
||||
</Button>
|
||||
<div className='spacer' />
|
||||
<img
|
||||
alt='An array of credit cards'
|
||||
border='0'
|
||||
src='//i.imgur.com/Q2SdSZG.png'
|
||||
style={{
|
||||
width: '100%'
|
||||
}} />
|
||||
</form>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className='spacer' />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, bindableActions)(JobTotal);
|
@ -1,150 +0,0 @@
|
||||
import React, { cloneElement, PropTypes } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { Button, Row, Col } from 'react-bootstrap';
|
||||
|
||||
import contain from '../../../utils/professor-x';
|
||||
import ListJobs from './List.jsx';
|
||||
|
||||
import {
|
||||
findJob,
|
||||
fetchJobs
|
||||
} from '../redux/actions';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
state => state.jobsApp.jobs.entities,
|
||||
state => state.jobsApp.jobs.results,
|
||||
state => state.jobsApp,
|
||||
(jobsMap, jobsById) => {
|
||||
return { jobs: jobsById.map(id => jobsMap[id]) };
|
||||
}
|
||||
);
|
||||
|
||||
const bindableActions = {
|
||||
findJob,
|
||||
fetchJobs
|
||||
};
|
||||
|
||||
const fetchOptions = {
|
||||
fetchAction: 'fetchJobs',
|
||||
isPrimed({ jobs }) {
|
||||
return jobs.length > 1;
|
||||
}
|
||||
};
|
||||
|
||||
export class Jobs extends PureComponent {
|
||||
static displayName = 'Jobs';
|
||||
|
||||
static propTypes = {
|
||||
push: PropTypes.func,
|
||||
findJob: PropTypes.func,
|
||||
fetchJobs: PropTypes.func,
|
||||
children: PropTypes.element,
|
||||
jobs: PropTypes.array,
|
||||
showModal: PropTypes.bool
|
||||
};
|
||||
|
||||
createJobClickHandler() {
|
||||
const { findJob } = this.props;
|
||||
|
||||
return (id) => {
|
||||
findJob(id);
|
||||
};
|
||||
}
|
||||
|
||||
renderList(handleJobClick, jobs) {
|
||||
return (
|
||||
<ListJobs
|
||||
handleClick={ handleJobClick }
|
||||
jobs={ jobs }/>
|
||||
);
|
||||
}
|
||||
|
||||
renderChild(child, jobs) {
|
||||
if (!child) {
|
||||
return null;
|
||||
}
|
||||
return cloneElement(
|
||||
child,
|
||||
{ jobs }
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
jobs
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col
|
||||
md={ 10 }
|
||||
mdOffset= { 1 }
|
||||
xs={ 12 }>
|
||||
<h1 className='text-center'>
|
||||
Hire a JavaScript engineer who's experienced in HTML5,
|
||||
Node.js, MongoDB, and Agile Development.
|
||||
</h1>
|
||||
<div className='spacer' />
|
||||
<Row className='text-center'>
|
||||
<Col
|
||||
sm={ 8 }
|
||||
smOffset={ 2 }
|
||||
xs={ 12 }>
|
||||
<LinkContainer to='/jobs/new' >
|
||||
<Button className='signup-btn btn-block btn-cta'>
|
||||
Post a job: $1,000
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
<div className='spacer' />
|
||||
</Col>
|
||||
</Row>
|
||||
<div className='spacer' />
|
||||
<Row>
|
||||
<Col
|
||||
md={ 2 }
|
||||
xs={ 4 }>
|
||||
<img
|
||||
alt={`
|
||||
a photo of Michael Gai, who recently hired a software
|
||||
engineer through Free Code Camp
|
||||
`}
|
||||
className='img-responsive testimonial-image-jobs img-center'
|
||||
src='//i.imgur.com/tGcAA8H.jpg' />
|
||||
</Col>
|
||||
<Col
|
||||
md={ 10 }
|
||||
xs={ 8 }>
|
||||
<blockquote>
|
||||
<p>
|
||||
We hired our last developer out of Free Code Camp
|
||||
and couldn't be happier. Free Code Camp is now
|
||||
our go-to way to bring on pre-screened candidates
|
||||
who are enthusiastic about learning quickly and
|
||||
becoming immediately productive in their new career.
|
||||
</p>
|
||||
<footer>
|
||||
Michael Gai, <cite>CEO at CoNarrative</cite>
|
||||
</footer>
|
||||
</blockquote>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
{ this.renderChild(children, jobs) ||
|
||||
this.renderList(this.createJobClickHandler(), jobs) }
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, bindableActions),
|
||||
contain(fetchOptions)
|
||||
)(Jobs);
|
@ -1,86 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
|
||||
export default class ListJobs extends PureComponent {
|
||||
static displayName = 'ListJobs';
|
||||
|
||||
static propTypes = {
|
||||
handleClick: PropTypes.func,
|
||||
jobs: PropTypes.array
|
||||
};
|
||||
|
||||
addLocation(locale) {
|
||||
if (!locale) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className='hidden-xs hidden-sm'>
|
||||
{ locale }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderJobs(handleClick, jobs = []) {
|
||||
return jobs
|
||||
.filter(({ isPaid, isApproved, isFilled }) => {
|
||||
return isPaid && isApproved && !isFilled;
|
||||
})
|
||||
.map(({
|
||||
id,
|
||||
company,
|
||||
position,
|
||||
isHighlighted,
|
||||
locale
|
||||
}) => {
|
||||
|
||||
const className = classnames({
|
||||
'jobs-list': true,
|
||||
'col-xs-12': true,
|
||||
'jobs-list-highlight': isHighlighted
|
||||
});
|
||||
|
||||
const to = `/jobs/${id}`;
|
||||
|
||||
return (
|
||||
<LinkContainer
|
||||
key={ id }
|
||||
to={ to }>
|
||||
<ListGroupItem
|
||||
className={ className }
|
||||
onClick={ () => handleClick(id) }>
|
||||
<div>
|
||||
<h4 className='pull-left' style={{ display: 'inline-block' }}>
|
||||
<bold>{ company }</bold>
|
||||
{' '}
|
||||
<span className='hidden-xs hidden-sm'>
|
||||
- { position }
|
||||
</span>
|
||||
</h4>
|
||||
<h4
|
||||
className='pull-right'
|
||||
style={{ display: 'inline-block' }}>
|
||||
{ this.addLocation(locale) }
|
||||
</h4>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
</LinkContainer>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
handleClick,
|
||||
jobs
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ListGroup>
|
||||
{ this.renderJobs(handleClick, jobs) }
|
||||
</ListGroup>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,391 +0,0 @@
|
||||
import { helpers } from 'rx';
|
||||
import React, { PropTypes } from 'react';
|
||||
import { push } from 'react-router-redux';
|
||||
import { reduxForm } from 'redux-form';
|
||||
// import debug from 'debug';
|
||||
import dedent from 'dedent';
|
||||
|
||||
import {
|
||||
isAscii,
|
||||
isEmail,
|
||||
isURL
|
||||
} from 'validator';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Input,
|
||||
Row
|
||||
} from 'react-bootstrap';
|
||||
|
||||
import { saveForm, loadSavedForm } from '../redux/actions';
|
||||
|
||||
// const log = debug('fcc:jobs:newForm');
|
||||
|
||||
const hightlightCopy = `
|
||||
Highlight my post to make it stand out. (+$250)
|
||||
`;
|
||||
|
||||
const isRemoteCopy = `
|
||||
This job can be performed remotely.
|
||||
`;
|
||||
|
||||
const howToApplyCopy = dedent`
|
||||
Examples: click here to apply yourcompany.com/jobs/33
|
||||
Or email jobs@yourcompany.com
|
||||
`;
|
||||
|
||||
const checkboxClass = dedent`
|
||||
text-left
|
||||
jobs-checkbox-spacer
|
||||
col-sm-offset-2
|
||||
col-sm-6 col-md-offset-3
|
||||
`;
|
||||
|
||||
const certTypes = {
|
||||
isFrontEndCert: 'isFrontEndCert',
|
||||
isBackEndCert: 'isBackEndCert'
|
||||
};
|
||||
|
||||
function isValidURL(data) {
|
||||
return isURL(data, { require_protocol: true });
|
||||
}
|
||||
|
||||
const fields = [
|
||||
'position',
|
||||
'locale',
|
||||
'description',
|
||||
'email',
|
||||
'url',
|
||||
'logo',
|
||||
'company',
|
||||
'isHighlighted',
|
||||
'isRemoteOk',
|
||||
'isFrontEndCert',
|
||||
'isBackEndCert',
|
||||
'howToApply'
|
||||
];
|
||||
|
||||
const fieldValidators = {
|
||||
position: makeRequired(isAscii),
|
||||
locale: makeRequired(isAscii),
|
||||
description: makeRequired(helpers.identity),
|
||||
email: makeRequired(isEmail),
|
||||
url: makeRequired(isValidURL),
|
||||
logo: makeOptional(isValidURL),
|
||||
company: makeRequired(isAscii),
|
||||
howToApply: makeRequired(isAscii)
|
||||
};
|
||||
|
||||
function makeOptional(validator) {
|
||||
return val => val ? validator(val) : true;
|
||||
}
|
||||
function makeRequired(validator) {
|
||||
return (val) => val ? validator(val) : false;
|
||||
}
|
||||
|
||||
function validateForm(values) {
|
||||
return Object.keys(fieldValidators)
|
||||
.map(field => {
|
||||
if (fieldValidators[field](values[field])) {
|
||||
return null;
|
||||
}
|
||||
return { [field]: !fieldValidators[field](values[field]) };
|
||||
})
|
||||
.filter(Boolean)
|
||||
.reduce((errors, error) => ({ ...errors, ...error }), {});
|
||||
}
|
||||
|
||||
function getBsStyle(field) {
|
||||
if (field.pristine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return field.error ?
|
||||
'error' :
|
||||
'success';
|
||||
}
|
||||
|
||||
export class NewJob extends React.Component {
|
||||
static displayName = 'NewJob';
|
||||
|
||||
static propTypes = {
|
||||
fields: PropTypes.object,
|
||||
handleSubmit: PropTypes.func,
|
||||
loadSavedForm: PropTypes.func,
|
||||
push: PropTypes.func,
|
||||
saveForm: PropTypes.func
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadSavedForm();
|
||||
}
|
||||
|
||||
handleSubmit(job) {
|
||||
this.props.saveForm(job);
|
||||
this.props.push('/jobs/new/preview');
|
||||
}
|
||||
|
||||
handleCertClick(name) {
|
||||
const { fields } = this.props;
|
||||
Object.keys(certTypes).forEach(certType => {
|
||||
if (certType === name) {
|
||||
return fields[certType].onChange(true);
|
||||
}
|
||||
return fields[certType].onChange(false);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
fields: {
|
||||
position,
|
||||
locale,
|
||||
description,
|
||||
email,
|
||||
url,
|
||||
logo,
|
||||
company,
|
||||
isHighlighted,
|
||||
isRemoteOk,
|
||||
howToApply,
|
||||
isFrontEndCert,
|
||||
isBackEndCert
|
||||
},
|
||||
handleSubmit
|
||||
} = this.props;
|
||||
|
||||
const { handleChange } = this;
|
||||
const labelClass = 'col-sm-offset-1 col-sm-2';
|
||||
const inputClass = 'col-sm-6';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<Col
|
||||
md={ 10 }
|
||||
mdOffset={ 1 }>
|
||||
<div className='text-center'>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
onSubmit={ handleSubmit(data => this.handleSubmit(data)) }>
|
||||
|
||||
<div className='spacer'>
|
||||
<h2>First, select your ideal applicant: </h2>
|
||||
</div>
|
||||
|
||||
<Row>
|
||||
<Col
|
||||
xs={ 6 }
|
||||
xsOffset={ 3 }>
|
||||
<Row>
|
||||
<Button
|
||||
bsStyle='primary'
|
||||
className={ isFrontEndCert.value ? 'active' : '' }
|
||||
onClick={ () => {
|
||||
if (!isFrontEndCert.value) {
|
||||
this.handleCertClick(certTypes.isFrontEndCert);
|
||||
}
|
||||
}}>
|
||||
<h4>Front End Development Certified</h4>
|
||||
You can expect each applicant
|
||||
to have a code portfolio using the
|
||||
following technologies:
|
||||
HTML5, CSS, jQuery, API integrations
|
||||
<br />
|
||||
<br />
|
||||
</Button>
|
||||
</Row>
|
||||
<div className='button-spacer' />
|
||||
<Row>
|
||||
<Button
|
||||
bsStyle='primary'
|
||||
className={ isBackEndCert.value ? 'active' : ''}
|
||||
onClick={ () => {
|
||||
if (!isBackEndCert.value) {
|
||||
this.handleCertClick(certTypes.isBackEndCert);
|
||||
}
|
||||
}}>
|
||||
<h4>Back End Development Certified</h4>
|
||||
You can expect each applicant to have a code
|
||||
portfolio using the following technologies:
|
||||
HTML5, CSS, jQuery, API integrations, MVC Framework,
|
||||
JavaScript, Node.js, MongoDB, Express.js
|
||||
<br />
|
||||
<br />
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className='spacer'>
|
||||
<h2>Tell us about the position</h2>
|
||||
</div>
|
||||
<hr />
|
||||
<Input
|
||||
bsStyle={ getBsStyle(position) }
|
||||
label='Job Title'
|
||||
labelClassName={ labelClass }
|
||||
placeholder={
|
||||
'e.g. Full Stack Developer, Front End Developer, etc.'
|
||||
}
|
||||
required={ true }
|
||||
type='text'
|
||||
wrapperClassName={ inputClass }
|
||||
{ ...position }
|
||||
/>
|
||||
<Input
|
||||
bsStyle={ getBsStyle(locale) }
|
||||
label='Location'
|
||||
labelClassName={ labelClass }
|
||||
placeholder='e.g. San Francisco, Remote, etc.'
|
||||
required={ true }
|
||||
type='text'
|
||||
wrapperClassName={ inputClass }
|
||||
{ ...locale }
|
||||
/>
|
||||
<Input
|
||||
bsStyle={ getBsStyle(description) }
|
||||
label='Description'
|
||||
labelClassName={ labelClass }
|
||||
required={ true }
|
||||
rows='10'
|
||||
type='textarea'
|
||||
wrapperClassName={ inputClass }
|
||||
{ ...description }
|
||||
/>
|
||||
<Input
|
||||
label={ isRemoteCopy }
|
||||
type='checkbox'
|
||||
wrapperClassName={ checkboxClass }
|
||||
{ ...isRemoteOk }
|
||||
/>
|
||||
<div className='spacer' />
|
||||
|
||||
<hr />
|
||||
<Row>
|
||||
<div>
|
||||
<h2>How should they apply?</h2>
|
||||
</div>
|
||||
<Input
|
||||
bsStyle={ getBsStyle(howToApply) }
|
||||
label=' '
|
||||
labelClassName={ labelClass }
|
||||
placeholder={ howToApplyCopy }
|
||||
required={ true }
|
||||
rows='2'
|
||||
type='textarea'
|
||||
wrapperClassName={ inputClass }
|
||||
{ ...howToApply }
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<div className='spacer' />
|
||||
<hr />
|
||||
<div>
|
||||
<h2>Tell us about your organization</h2>
|
||||
</div>
|
||||
<Input
|
||||
bsStyle={ getBsStyle(company) }
|
||||
label='Company Name'
|
||||
labelClassName={ labelClass }
|
||||
onChange={ (e) => handleChange('company', e) }
|
||||
type='text'
|
||||
wrapperClassName={ inputClass }
|
||||
{ ...company }
|
||||
/>
|
||||
<Input
|
||||
bsStyle={ getBsStyle(email) }
|
||||
label='Email'
|
||||
labelClassName={ labelClass }
|
||||
placeholder='This is how we will contact you'
|
||||
required={ true }
|
||||
type='email'
|
||||
wrapperClassName={ inputClass }
|
||||
{ ...email }
|
||||
/>
|
||||
<Input
|
||||
bsStyle={ getBsStyle(url) }
|
||||
label='URL'
|
||||
labelClassName={ labelClass }
|
||||
placeholder='http://yourcompany.com'
|
||||
type='url'
|
||||
wrapperClassName={ inputClass }
|
||||
{ ...url }
|
||||
/>
|
||||
<Input
|
||||
bsStyle={ getBsStyle(logo) }
|
||||
label='Logo'
|
||||
labelClassName={ labelClass }
|
||||
placeholder='http://yourcompany.com/logo.png'
|
||||
type='url'
|
||||
wrapperClassName={ inputClass }
|
||||
{ ...logo }
|
||||
/>
|
||||
|
||||
<div className='spacer' />
|
||||
<hr />
|
||||
<div>
|
||||
<div>
|
||||
<h2>Make it stand out</h2>
|
||||
</div>
|
||||
<div className='spacer' />
|
||||
<Row>
|
||||
<Col
|
||||
md={ 6 }
|
||||
mdOffset={ 3 }>
|
||||
Highlight this ad to give it extra attention.
|
||||
<br />
|
||||
Featured listings receive more clicks and more applications.
|
||||
</Col>
|
||||
</Row>
|
||||
<div className='spacer' />
|
||||
<Row>
|
||||
<Input
|
||||
bsSize='large'
|
||||
bsStyle='success'
|
||||
label={ hightlightCopy }
|
||||
type='checkbox'
|
||||
wrapperClassName={
|
||||
checkboxClass.replace('text-left', '')
|
||||
}
|
||||
{ ...isHighlighted }
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Row>
|
||||
<Col
|
||||
className='text-left'
|
||||
lg={ 6 }
|
||||
lgOffset={ 3 }>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
type='submit'>
|
||||
Preview My Ad
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</form>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default reduxForm(
|
||||
{
|
||||
form: 'NewJob',
|
||||
fields,
|
||||
validate: validateForm
|
||||
},
|
||||
state => ({ initialValues: state.jobsApp.initialValues }),
|
||||
{
|
||||
loadSavedForm,
|
||||
push,
|
||||
saveForm
|
||||
}
|
||||
)(NewJob);
|
@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import { Button, Col, Row } from 'react-bootstrap';
|
||||
|
||||
export default class extends React.createClass {
|
||||
static displayName = 'NewJobCompleted';
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<div>
|
||||
<Row>
|
||||
<h1>
|
||||
Your Position has Been Submitted
|
||||
</h1>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col
|
||||
md={ 6 }
|
||||
mdOffset={ 3 }>
|
||||
We’ll review your listing and email you when it’s live.
|
||||
<br />
|
||||
Thank you for listing this job with Free Code Camp.
|
||||
</Col>
|
||||
</Row>
|
||||
<div className='spacer' />
|
||||
<LinkContainer to={ '/jobs' }>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='large'
|
||||
bsStyle='primary'>
|
||||
Go to the job board
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
import { CompositeDisposable } from 'rx';
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Button, Row, Col } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { goBack, push } from 'react-router-redux';
|
||||
|
||||
import ShowJob from './ShowJob.jsx';
|
||||
import JobNotFound from './JobNotFound.jsx';
|
||||
|
||||
import { clearForm, saveJob } from '../redux/actions';
|
||||
|
||||
const mapStateToProps = state => ({ job: state.jobsApp.newJob });
|
||||
|
||||
const bindableActions = {
|
||||
goBack,
|
||||
push,
|
||||
clearForm,
|
||||
saveJob
|
||||
};
|
||||
|
||||
export class JobPreview extends PureComponent {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this._subscriptions = new CompositeDisposable();
|
||||
}
|
||||
|
||||
static displayName = 'Preview';
|
||||
|
||||
static propTypes = {
|
||||
job: PropTypes.object,
|
||||
saveJob: PropTypes.func,
|
||||
clearForm: PropTypes.func,
|
||||
push: PropTypes.func
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
const { push, job } = this.props;
|
||||
// redirect user in client
|
||||
if (!job || !job.position || !job.description) {
|
||||
push('/jobs/new');
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._subscriptions.dispose();
|
||||
}
|
||||
|
||||
handleJobSubmit() {
|
||||
const { clearForm, saveJob, job } = this.props;
|
||||
clearForm();
|
||||
const subscription = saveJob(job).subscribe();
|
||||
this._subscriptions.add(subscription);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { job, goBack } = this.props;
|
||||
|
||||
if (!job || !job.position || !job.description) {
|
||||
return <JobNotFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ShowJob job={ job } />
|
||||
<div className='spacer'></div>
|
||||
<hr />
|
||||
<Row>
|
||||
<Col
|
||||
md={ 10 }
|
||||
mdOffset={ 1 }
|
||||
xs={ 12 }>
|
||||
<div>
|
||||
<Button
|
||||
block={ true }
|
||||
className='signup-btn'
|
||||
onClick={ () => this.handleJobSubmit() }>
|
||||
|
||||
Looks great! Let's Check Out
|
||||
</Button>
|
||||
<Button
|
||||
block={ true }
|
||||
onClick={ goBack } >
|
||||
Head back and make edits
|
||||
</Button>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, bindableActions)(JobPreview);
|
@ -1,146 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { push } from 'react-router-redux';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import contain from '../../../utils/professor-x';
|
||||
import { fetchJobs } from '../redux/actions';
|
||||
|
||||
import ShowJob from './ShowJob.jsx';
|
||||
import JobNotFound from './JobNotFound.jsx';
|
||||
import { isJobValid } from '../utils';
|
||||
|
||||
function shouldShowApply(
|
||||
{
|
||||
isFrontEndCert: isFrontEndCertReq = false,
|
||||
isBackEndCert: isBackEndCertReq = false
|
||||
}, {
|
||||
isFrontEndCert = false,
|
||||
isBackEndCert = false
|
||||
}
|
||||
) {
|
||||
return (!isFrontEndCertReq && !isBackEndCertReq) ||
|
||||
(isBackEndCertReq && isBackEndCert) ||
|
||||
(isFrontEndCertReq && isFrontEndCert);
|
||||
}
|
||||
|
||||
function generateMessage(
|
||||
{
|
||||
isFrontEndCert: isFrontEndCertReq = false,
|
||||
isBackEndCert: isBackEndCertReq = false
|
||||
},
|
||||
{
|
||||
isFrontEndCert = false,
|
||||
isBackEndCert = false,
|
||||
isSignedIn = false
|
||||
}
|
||||
) {
|
||||
|
||||
if (!isSignedIn) {
|
||||
return 'Must be signed in to apply';
|
||||
}
|
||||
if (isFrontEndCertReq && !isFrontEndCert) {
|
||||
return 'This employer requires Free Code Camp’s Front ' +
|
||||
'End Development Certification in order to apply';
|
||||
}
|
||||
if (isBackEndCertReq && !isBackEndCert) {
|
||||
return 'This employer requires Free Code Camp’s Back ' +
|
||||
'End Development Certification in order to apply';
|
||||
}
|
||||
if (isFrontEndCertReq && isFrontEndCertReq) {
|
||||
return 'This employer requires the Front End Development Certification. ' +
|
||||
"You've earned it, so feel free to apply.";
|
||||
}
|
||||
return 'This employer requires the Back End Development Certification. ' +
|
||||
"You've earned it, so feel free to apply.";
|
||||
}
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
state => state.app,
|
||||
state => state.jobsApp.currentJob,
|
||||
state => state.jobsApp.jobs.entities,
|
||||
({ username, isFrontEndCert, isBackEndCert }, currentJob, jobs) => ({
|
||||
username,
|
||||
isFrontEndCert,
|
||||
isBackEndCert,
|
||||
job: jobs[currentJob] || {}
|
||||
})
|
||||
);
|
||||
|
||||
const bindableActions = {
|
||||
push,
|
||||
fetchJobs
|
||||
};
|
||||
|
||||
const fetchOptions = {
|
||||
fetchAction: 'fetchJobs',
|
||||
getActionArgs({ params: { id } }) {
|
||||
return [ id ];
|
||||
},
|
||||
isPrimed({ params: { id } = {}, job = {} }) {
|
||||
return job.id === id;
|
||||
},
|
||||
// using es6 destructuring
|
||||
shouldRefetch({ job }, { params: { id } }) {
|
||||
return job.id !== id;
|
||||
}
|
||||
};
|
||||
|
||||
export class Show extends PureComponent {
|
||||
static displayName = 'Show';
|
||||
|
||||
static propTypes = {
|
||||
job: PropTypes.object,
|
||||
isBackEndCert: PropTypes.bool,
|
||||
isFrontEndCert: PropTypes.bool,
|
||||
username: PropTypes.string
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { job, push } = this.props;
|
||||
// redirect user in client
|
||||
if (!isJobValid(job)) {
|
||||
push('/jobs');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isBackEndCert,
|
||||
isFrontEndCert,
|
||||
job,
|
||||
username
|
||||
} = this.props;
|
||||
|
||||
if (!isJobValid(job)) {
|
||||
return <JobNotFound />;
|
||||
}
|
||||
|
||||
const isSignedIn = !!username;
|
||||
|
||||
const showApply = shouldShowApply(
|
||||
job,
|
||||
{ isFrontEndCert, isBackEndCert }
|
||||
);
|
||||
|
||||
const message = generateMessage(
|
||||
job,
|
||||
{ isFrontEndCert, isBackEndCert, isSignedIn }
|
||||
);
|
||||
|
||||
return (
|
||||
<ShowJob
|
||||
message={ message }
|
||||
preview={ false }
|
||||
showApply={ showApply }
|
||||
{ ...this.props }/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, bindableActions),
|
||||
contain(fetchOptions)
|
||||
)(Show);
|
@ -1,145 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Row, Col, Thumbnail } from 'react-bootstrap';
|
||||
import urlRegexFactory from 'url-regex';
|
||||
|
||||
const urlRegex = urlRegexFactory();
|
||||
const defaultImage =
|
||||
'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png';
|
||||
|
||||
const thumbnailStyle = {
|
||||
backgroundColor: 'white',
|
||||
maxHeight: '100px',
|
||||
maxWidth: '100px'
|
||||
};
|
||||
|
||||
function addATags(text) {
|
||||
return text.replace(urlRegex, function(match) {
|
||||
return `<a href=${match} target='_blank'>${match}</a>`;
|
||||
});
|
||||
}
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'ShowJob',
|
||||
propTypes: {
|
||||
job: PropTypes.object,
|
||||
params: PropTypes.object,
|
||||
showApply: PropTypes.bool,
|
||||
preview: PropTypes.bool,
|
||||
message: PropTypes.string
|
||||
},
|
||||
|
||||
renderHeader({ company, position }) {
|
||||
return (
|
||||
<div>
|
||||
<h4 style={{ display: 'inline-block' }}>{ company }</h4>
|
||||
<h5
|
||||
className='pull-right hidden-xs hidden-md'
|
||||
style={{ display: 'inline-block' }}>
|
||||
{ position }
|
||||
</h5>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderHowToApply(showApply, preview, message, howToApply) {
|
||||
if (!showApply) {
|
||||
return (
|
||||
<Row>
|
||||
<Col
|
||||
md={ 6 }
|
||||
mdOffset={ 3 }>
|
||||
<h4 className='bg-info text-center'>{ message }</h4>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<hr />
|
||||
<Col
|
||||
md={ 6 }
|
||||
mdOffset={ 3 }>
|
||||
<div>
|
||||
<bold>{ preview ? 'How do I apply?' : message }</bold>
|
||||
<br />
|
||||
<br />
|
||||
<span dangerouslySetInnerHTML={{
|
||||
__html: addATags(howToApply)
|
||||
}} />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
showApply = true,
|
||||
message,
|
||||
preview = true,
|
||||
job = {}
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
logo,
|
||||
position,
|
||||
city,
|
||||
company,
|
||||
state,
|
||||
locale,
|
||||
description,
|
||||
howToApply
|
||||
} = job;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<Col
|
||||
md={ 10 }
|
||||
mdOffset={ 1 }
|
||||
xs={ 12 }>
|
||||
<div>
|
||||
<Row>
|
||||
<h2 className='text-center'>
|
||||
{ company }
|
||||
</h2>
|
||||
</Row>
|
||||
<div className='spacer' />
|
||||
<Row>
|
||||
<Col
|
||||
md={ 2 }
|
||||
mdOffset={ 3 }>
|
||||
<Thumbnail
|
||||
alt={ logo ? company + 'company logo' : 'stock image' }
|
||||
src={ logo || defaultImage }
|
||||
style={ thumbnailStyle } />
|
||||
</Col>
|
||||
<Col
|
||||
md={ 4 }>
|
||||
|
||||
<bold>Position: </bold> { position || 'N/A' }
|
||||
<br />
|
||||
<bold>Location: </bold>
|
||||
{ locale ? locale : `${city}, ${state}` }
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
<div className='spacer' />
|
||||
<Row>
|
||||
<Col
|
||||
md={ 6 }
|
||||
mdOffset={ 3 }
|
||||
style={{ whiteSpace: 'pre-line' }}
|
||||
xs={ 12 }>
|
||||
<p>{ description }</p>
|
||||
</Col>
|
||||
</Row>
|
||||
{ this.renderHowToApply(showApply, preview, message, howToApply) }
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
@ -1,34 +0,0 @@
|
||||
import Jobs from './components/Jobs.jsx';
|
||||
import NewJob from './components/NewJob.jsx';
|
||||
import Show from './components/Show.jsx';
|
||||
import Preview from './components/Preview.jsx';
|
||||
import JobTotal from './components/JobTotal.jsx';
|
||||
import NewJobCompleted from './components/NewJobCompleted.jsx';
|
||||
|
||||
/*
|
||||
* index: /jobs list jobs
|
||||
* show: /jobs/:id show one job
|
||||
* create /jobs/new create a new job
|
||||
*/
|
||||
|
||||
export default {
|
||||
childRoutes: [{
|
||||
path: '/jobs',
|
||||
component: Jobs
|
||||
}, {
|
||||
path: 'jobs/new',
|
||||
component: NewJob
|
||||
}, {
|
||||
path: 'jobs/new/preview',
|
||||
component: Preview
|
||||
}, {
|
||||
path: 'jobs/new/check-out',
|
||||
component: JobTotal
|
||||
}, {
|
||||
path: 'jobs/new/completed',
|
||||
component: NewJobCompleted
|
||||
}, {
|
||||
path: 'jobs/:id',
|
||||
component: Show
|
||||
}]
|
||||
};
|
@ -1,34 +0,0 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
|
||||
import types from './types';
|
||||
|
||||
export const fetchJobs = createAction(types.fetchJobs);
|
||||
export const fetchJobsCompleted = createAction(
|
||||
types.fetchJobsCompleted,
|
||||
(currentJob, jobs) => ({ currentJob, jobs })
|
||||
);
|
||||
|
||||
export const findJob = createAction(types.findJob);
|
||||
|
||||
// saves to database
|
||||
export const saveJob = createAction(types.saveJob);
|
||||
// saves to localStorage
|
||||
export const saveForm = createAction(types.saveForm);
|
||||
|
||||
export const saveCompleted = createAction(types.saveCompleted);
|
||||
|
||||
export const clearForm = createAction(types.clearForm);
|
||||
|
||||
export const loadSavedForm = createAction(types.loadSavedForm);
|
||||
export const loadSavedFormCompleted = createAction(
|
||||
types.loadSavedFormCompleted
|
||||
);
|
||||
|
||||
export const clearPromo = createAction(types.clearPromo);
|
||||
export const updatePromo = createAction(
|
||||
types.updatePromo,
|
||||
({ target: { value = '' } = {} } = {}) => value
|
||||
);
|
||||
|
||||
export const applyPromo = createAction(types.applyPromo);
|
||||
export const applyPromoCompleted = createAction(types.applyPromoCompleted);
|
@ -1,39 +0,0 @@
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { applyPromo } from './types';
|
||||
import { applyPromoCompleted } from './actions';
|
||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||
|
||||
export default () => ({ dispatch }) => next => {
|
||||
return function applyPromoSaga(action) {
|
||||
if (action.type !== applyPromo) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const { id, code = '', type = null } = action.payload;
|
||||
|
||||
const body = {
|
||||
id,
|
||||
code: code.replace(/[^\d\w\s]/, '')
|
||||
};
|
||||
|
||||
if (type) {
|
||||
body.type = type;
|
||||
}
|
||||
|
||||
return postJSON$('/api/promos/getButton', body)
|
||||
.retry(3)
|
||||
.map(({ promo }) => {
|
||||
if (!promo || !promo.buttonId) {
|
||||
throw new Error('No promo returned by server');
|
||||
}
|
||||
|
||||
return applyPromoCompleted(promo);
|
||||
})
|
||||
.catch(error => Observable.just({
|
||||
type: 'app.handleError',
|
||||
error
|
||||
}))
|
||||
.doOnNext(dispatch);
|
||||
};
|
||||
};
|
@ -1,50 +0,0 @@
|
||||
import { Observable } from 'rx';
|
||||
import { normalize, Schema, arrayOf } from 'normalizr';
|
||||
|
||||
import { fetchJobsCompleted } from './actions';
|
||||
import { fetchJobs } from './types';
|
||||
import { handleError } from '../../../redux/types';
|
||||
|
||||
const job = new Schema('job', { idAttribute: 'id' });
|
||||
|
||||
export default ({ services }) => ({ dispatch }) => next => {
|
||||
return function fetchJobsSaga(action) {
|
||||
if (action.type !== fetchJobs) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const { payload: id } = action;
|
||||
const data = { service: 'jobs' };
|
||||
if (id) {
|
||||
data.params = { id };
|
||||
}
|
||||
|
||||
return services.readService$(data)
|
||||
.map(jobs => {
|
||||
if (!Array.isArray(jobs)) {
|
||||
jobs = [jobs];
|
||||
}
|
||||
|
||||
const { entities, result } = normalize(
|
||||
{ jobs },
|
||||
{ jobs: arrayOf(job) }
|
||||
);
|
||||
|
||||
|
||||
return fetchJobsCompleted(
|
||||
result.jobs[0],
|
||||
{
|
||||
entities: entities.job,
|
||||
results: result.jobs
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
return Observable.just({
|
||||
type: handleError,
|
||||
error
|
||||
});
|
||||
})
|
||||
.doOnNext(dispatch);
|
||||
};
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
export actions from './actions';
|
||||
export reducer from './reducer';
|
||||
export types from './types';
|
||||
|
||||
import fetchJobsSaga from './fetch-jobs-saga';
|
||||
import saveJobSaga from './save-job-saga';
|
||||
import applyPromoSaga from './apply-promo-saga';
|
||||
|
||||
export formNormalizer from './jobs-form-normalizer';
|
||||
|
||||
export const sagas = [ fetchJobsSaga, saveJobSaga, applyPromoSaga ];
|
@ -1,42 +0,0 @@
|
||||
import normalizeUrl from 'normalize-url';
|
||||
import {
|
||||
inHTMLData,
|
||||
uriInSingleQuotedAttr
|
||||
} from 'xss-filters';
|
||||
|
||||
const normalizeOptions = {
|
||||
stripWWW: false
|
||||
};
|
||||
|
||||
function ifDefinedNormalize(normalizer) {
|
||||
return value => value ? normalizer(value) : value;
|
||||
}
|
||||
|
||||
function formatUrl(url) {
|
||||
if (
|
||||
typeof url === 'string' &&
|
||||
url.length > 4 &&
|
||||
url.indexOf('.') !== -1
|
||||
) {
|
||||
// prevent trailing / from being stripped during typing
|
||||
let lastChar = '';
|
||||
if (url.substring(url.length - 1) === '/') {
|
||||
lastChar = '/';
|
||||
}
|
||||
return normalizeUrl(url, normalizeOptions) + lastChar;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export default {
|
||||
NewJob: {
|
||||
position: ifDefinedNormalize(inHTMLData),
|
||||
locale: ifDefinedNormalize(inHTMLData),
|
||||
description: ifDefinedNormalize(inHTMLData),
|
||||
email: ifDefinedNormalize(inHTMLData),
|
||||
url: ifDefinedNormalize(value => formatUrl(uriInSingleQuotedAttr(value))),
|
||||
logo: ifDefinedNormalize(value => formatUrl(uriInSingleQuotedAttr(value))),
|
||||
company: ifDefinedNormalize(inHTMLData),
|
||||
howToApply: ifDefinedNormalize(inHTMLData)
|
||||
}
|
||||
};
|
@ -1,85 +0,0 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
|
||||
import types from './types';
|
||||
|
||||
const replaceMethod = ''.replace;
|
||||
function replace(str) {
|
||||
if (!str) { return ''; }
|
||||
return replaceMethod.call(str, /[^\d\w\s]/, '');
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
// used by NewJob form
|
||||
initialValues: {},
|
||||
currentJob: '',
|
||||
newJob: {},
|
||||
jobs: {
|
||||
entities: {},
|
||||
results: []
|
||||
}
|
||||
};
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[types.findJob]: (state, { payload: id }) => {
|
||||
const currentJob = state.jobs.entities[id];
|
||||
return {
|
||||
...state,
|
||||
currentJob: currentJob && currentJob.id ?
|
||||
currentJob.id :
|
||||
state.currentJob
|
||||
};
|
||||
},
|
||||
[types.fetchJobsCompleted]: (state, { payload: { jobs, currentJob } }) => ({
|
||||
...state,
|
||||
currentJob,
|
||||
jobs
|
||||
}),
|
||||
[types.updatePromo]: (state, { payload }) => ({
|
||||
...state,
|
||||
promoCode: replace(payload)
|
||||
}),
|
||||
[types.saveCompleted]: (state, { payload: newJob }) => {
|
||||
return {
|
||||
...state,
|
||||
newJob
|
||||
};
|
||||
},
|
||||
[types.loadSavedFormCompleted]: (state, { payload: initialValues }) => ({
|
||||
...state,
|
||||
initialValues
|
||||
}),
|
||||
[types.applyPromoCompleted]: (state, { payload: promo }) => {
|
||||
|
||||
const {
|
||||
fullPrice: price,
|
||||
buttonId,
|
||||
discountAmount,
|
||||
code: promoCode,
|
||||
name: promoName
|
||||
} = promo;
|
||||
|
||||
return {
|
||||
...state,
|
||||
price,
|
||||
buttonId,
|
||||
discountAmount,
|
||||
promoCode,
|
||||
promoApplied: true,
|
||||
promoName
|
||||
};
|
||||
},
|
||||
[types.clearPromo]: state => ({
|
||||
/* eslint-disable no-undefined */
|
||||
...state,
|
||||
price: undefined,
|
||||
buttonId: undefined,
|
||||
discountAmount: undefined,
|
||||
promoCode: undefined,
|
||||
promoApplied: false,
|
||||
promoName: undefined
|
||||
/* eslint-enable no-undefined */
|
||||
})
|
||||
},
|
||||
initialState
|
||||
);
|
@ -1,32 +0,0 @@
|
||||
import { push } from 'react-router-redux';
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { saveCompleted } from './actions';
|
||||
import { saveJob } from './types';
|
||||
|
||||
import { handleError } from '../../../redux/types';
|
||||
|
||||
export default ({ services }) => ({ dispatch }) => next => {
|
||||
return function saveJobSaga(action) {
|
||||
const result = next(action);
|
||||
if (action.type !== saveJob) {
|
||||
return result;
|
||||
}
|
||||
const { payload: job } = action;
|
||||
|
||||
return services.createService$({
|
||||
service: 'jobs',
|
||||
params: { job }
|
||||
})
|
||||
.retry(3)
|
||||
.flatMap(job => Observable.of(
|
||||
saveCompleted(job),
|
||||
push('/jobs/new/check-out')
|
||||
))
|
||||
.catch(error => Observable.just({
|
||||
type: handleError,
|
||||
error
|
||||
}))
|
||||
.doOnNext(dispatch);
|
||||
};
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
import createTypes from '../../../utils/create-types';
|
||||
|
||||
export default createTypes([
|
||||
'fetchJobs',
|
||||
'fetchJobsCompleted',
|
||||
|
||||
'findJob',
|
||||
'saveJob',
|
||||
'saveForm',
|
||||
|
||||
'saveCompleted',
|
||||
|
||||
'clearForm',
|
||||
|
||||
'loadSavedForm',
|
||||
'loadSavedFormCompleted',
|
||||
|
||||
'clearPromo',
|
||||
'updatePromo',
|
||||
'applyPromo',
|
||||
'applyPromoCompleted'
|
||||
], 'jobs');
|
@ -1,29 +0,0 @@
|
||||
const defaults = {
|
||||
string: {
|
||||
value: '',
|
||||
valid: false,
|
||||
pristine: true,
|
||||
type: 'string'
|
||||
},
|
||||
bool: {
|
||||
value: false,
|
||||
type: 'boolean'
|
||||
}
|
||||
};
|
||||
|
||||
export function getDefaults(type, value) {
|
||||
if (!type) {
|
||||
return defaults['string'];
|
||||
}
|
||||
if (value) {
|
||||
return Object.assign({}, defaults[type], { value });
|
||||
}
|
||||
return Object.assign({}, defaults[type]);
|
||||
}
|
||||
|
||||
export function isJobValid(job) {
|
||||
return job &&
|
||||
!job.isFilled &&
|
||||
job.isApproved &&
|
||||
job.isPaid;
|
||||
}
|
80
common/app/routes/challenges/components/Bug-Modal.jsx
Normal file
80
common/app/routes/challenges/components/Bug-Modal.jsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Modal } from 'react-bootstrap';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { createIssue, openIssueSearch, closeBugModal } from '../redux/actions';
|
||||
|
||||
const mapStateToProps = state => ({ isOpen: state.challengesApp.isBugOpen });
|
||||
const actions = { createIssue, openIssueSearch, closeBugModal };
|
||||
const bugLink = 'https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/' +
|
||||
'FreeCodeCamp-Report-Bugs';
|
||||
|
||||
export class BugModal extends PureComponent {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool,
|
||||
closeBugModal: PropTypes.func,
|
||||
openIssueSearch: PropTypes.func,
|
||||
createIssue: PropTypes.func
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
closeBugModal,
|
||||
openIssueSearch,
|
||||
createIssue
|
||||
} = this.props;
|
||||
return (
|
||||
<Modal
|
||||
onHide={ closeBugModal }
|
||||
show={ isOpen }
|
||||
>
|
||||
<Modal.Header className='challenge-list-header'>
|
||||
Did you find a bug?
|
||||
<span className='close closing-x'>×</span>
|
||||
</Modal.Header>
|
||||
<Modal.Body className='text-center'>
|
||||
<h3>
|
||||
Before you submit a new issue,
|
||||
read "Help I've Found a Bug" and
|
||||
browse other issues with this challenge.
|
||||
</h3>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
href={ bugLink }
|
||||
target='_blank'
|
||||
>
|
||||
Read "Help I've Found a Bug"
|
||||
</Button>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
onClick={ openIssueSearch }
|
||||
>
|
||||
Browse other issues with this challenge
|
||||
</Button>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
onClick={ createIssue }
|
||||
>
|
||||
Create my GitHub issue
|
||||
</Button>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, actions)(BugModal);
|
114
common/app/routes/challenges/components/Show.jsx
Normal file
114
common/app/routes/challenges/components/Show.jsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { contain } from 'redux-epic';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
|
||||
import Classic from './classic/Classic.jsx';
|
||||
import Step from './step/Step.jsx';
|
||||
import Project from './project/Project.jsx';
|
||||
import Video from './video/Video.jsx';
|
||||
|
||||
import {
|
||||
fetchChallenge,
|
||||
fetchChallenges,
|
||||
replaceChallenge,
|
||||
resetUi
|
||||
} from '../redux/actions';
|
||||
import { challengeSelector } from '../redux/selectors';
|
||||
import { updateTitle } from '../../../redux/actions';
|
||||
|
||||
const views = {
|
||||
step: Step,
|
||||
classic: Classic,
|
||||
project: Project,
|
||||
simple: Project,
|
||||
video: Video
|
||||
};
|
||||
|
||||
const bindableActions = {
|
||||
fetchChallenge,
|
||||
fetchChallenges,
|
||||
replaceChallenge,
|
||||
resetUi,
|
||||
updateTitle
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
challengeSelector,
|
||||
state => state.challengesApp.challenge,
|
||||
state => state.challengesApp.superBlocks,
|
||||
({ viewType }, challenge, superBlocks = []) => ({
|
||||
challenge,
|
||||
viewType,
|
||||
areChallengesLoaded: superBlocks.length > 0
|
||||
})
|
||||
);
|
||||
|
||||
const fetchOptions = {
|
||||
fetchAction: 'fetchChallenge',
|
||||
getActionArgs({ params: { block, dashedName } }) {
|
||||
return [ dashedName, block ];
|
||||
},
|
||||
isPrimed({ challenge }) {
|
||||
return !!challenge;
|
||||
}
|
||||
};
|
||||
|
||||
export class Challenges extends PureComponent {
|
||||
static displayName = 'Challenges';
|
||||
|
||||
static propTypes = {
|
||||
isStep: PropTypes.bool,
|
||||
fetchChallenges: PropTypes.func.isRequired,
|
||||
replaceChallenge: PropTypes.func.isRequired,
|
||||
params: PropTypes.object.isRequired,
|
||||
areChallengesLoaded: PropTypes.bool,
|
||||
resetUi: PropTypes.func.isRequired,
|
||||
updateTitle: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.props.updateTitle(this.props.params.dashedName);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.areChallengesLoaded) {
|
||||
this.props.fetchChallenges();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.resetUi();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { block, dashedName } = nextProps.params;
|
||||
const { resetUi, updateTitle, replaceChallenge } = this.props;
|
||||
if (this.props.params.dashedName !== dashedName) {
|
||||
updateTitle(dashedName);
|
||||
resetUi();
|
||||
replaceChallenge({ dashedName, block });
|
||||
}
|
||||
}
|
||||
|
||||
renderView(viewType) {
|
||||
const View = views[viewType] || Classic;
|
||||
return <View />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { viewType } = this.props;
|
||||
return (
|
||||
<div>
|
||||
{ this.renderView(viewType) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, bindableActions),
|
||||
contain(fetchOptions)
|
||||
)(Challenges);
|
112
common/app/routes/challenges/components/classic/Classic.jsx
Normal file
112
common/app/routes/challenges/components/classic/Classic.jsx
Normal file
@ -0,0 +1,112 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Col } from 'react-bootstrap';
|
||||
import { createSelector } from 'reselect';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
|
||||
import Editor from './Editor.jsx';
|
||||
import SidePanel from './Side-Panel.jsx';
|
||||
import Preview from './Preview.jsx';
|
||||
import BugModal from '../Bug-Modal.jsx';
|
||||
import { challengeSelector } from '../../redux/selectors';
|
||||
import {
|
||||
executeChallenge,
|
||||
updateMain,
|
||||
updateFile,
|
||||
loadCode
|
||||
} from '../../redux/actions';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
challengeSelector,
|
||||
state => state.challengesApp.tests,
|
||||
state => state.challengesApp.files,
|
||||
state => state.challengesApp.key,
|
||||
(
|
||||
{ showPreview, mode },
|
||||
tests,
|
||||
files = {},
|
||||
key = ''
|
||||
) => ({
|
||||
content: files[key] && files[key].contents || '',
|
||||
file: files[key],
|
||||
showPreview,
|
||||
mode,
|
||||
tests
|
||||
})
|
||||
);
|
||||
|
||||
const bindableActions = {
|
||||
executeChallenge,
|
||||
updateFile,
|
||||
updateMain,
|
||||
loadCode
|
||||
};
|
||||
|
||||
export class Challenge extends PureComponent {
|
||||
static displayName = 'Challenge';
|
||||
|
||||
static propTypes = {
|
||||
showPreview: PropTypes.bool,
|
||||
content: PropTypes.string,
|
||||
mode: PropTypes.string,
|
||||
updateFile: PropTypes.func,
|
||||
executeChallenge: PropTypes.func,
|
||||
updateMain: PropTypes.func,
|
||||
loadCode: PropTypes.func
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadCode();
|
||||
this.props.updateMain();
|
||||
}
|
||||
|
||||
renderPreview(showPreview) {
|
||||
if (!showPreview) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Col
|
||||
lg={ 3 }
|
||||
md={ 4 }
|
||||
>
|
||||
<Preview />
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
content,
|
||||
updateFile,
|
||||
file,
|
||||
mode,
|
||||
showPreview,
|
||||
executeChallenge
|
||||
} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Col
|
||||
lg={ 3 }
|
||||
md={ showPreview ? 3 : 4 }
|
||||
>
|
||||
<SidePanel />
|
||||
</Col>
|
||||
<Col
|
||||
lg={ showPreview ? 6 : 9 }
|
||||
md={ showPreview ? 5 : 8 }
|
||||
>
|
||||
<Editor
|
||||
content={ content }
|
||||
executeChallenge={ executeChallenge }
|
||||
mode={ mode }
|
||||
updateFile={ content => updateFile(content, file) }
|
||||
/>
|
||||
</Col>
|
||||
{ this.renderPreview(showPreview) }
|
||||
<BugModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, bindableActions)(Challenge);
|
132
common/app/routes/challenges/components/classic/Editor.jsx
Normal file
132
common/app/routes/challenges/components/classic/Editor.jsx
Normal file
@ -0,0 +1,132 @@
|
||||
import { Subject } from 'rx';
|
||||
import React, { PropTypes } from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Codemirror from 'react-codemirror';
|
||||
import NoSSR from 'react-no-ssr';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
state => state.app.windowHeight,
|
||||
state => state.app.navHeight,
|
||||
(windowHeight, navHeight) => ({ height: windowHeight - navHeight - 50 })
|
||||
);
|
||||
|
||||
const editorDebounceTimeout = 750;
|
||||
|
||||
const options = {
|
||||
lint: true,
|
||||
lineNumbers: true,
|
||||
mode: 'javascript',
|
||||
theme: 'monokai',
|
||||
runnable: true,
|
||||
matchBrackets: true,
|
||||
autoCloseBrackets: true,
|
||||
scrollbarStyle: 'null',
|
||||
lineWrapping: true,
|
||||
gutters: ['CodeMirror-lint-markers']
|
||||
};
|
||||
|
||||
export class Editor extends PureComponent {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this._editorContent$ = new Subject();
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
static displayName = 'Editor';
|
||||
static propTypes = {
|
||||
executeChallenge: PropTypes.func,
|
||||
height: PropTypes.number,
|
||||
content: PropTypes.string,
|
||||
mode: PropTypes.string,
|
||||
updateFile: PropTypes.func
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
content: '// Happy Coding!',
|
||||
mode: 'javascript'
|
||||
};
|
||||
|
||||
createOptions = createSelector(
|
||||
state => state.options,
|
||||
state => state.executeChallenge,
|
||||
state => state.mode,
|
||||
(options, executeChallenge, mode) => ({
|
||||
...options,
|
||||
mode,
|
||||
extraKeys: {
|
||||
Tab(cm) {
|
||||
if (cm.somethingSelected()) {
|
||||
return cm.indentSelection('add');
|
||||
}
|
||||
const spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
|
||||
return cm.replaceSelection(spaces);
|
||||
},
|
||||
'Shift-Tab': function(cm) {
|
||||
if (cm.somethingSelected()) {
|
||||
return cm.indentSelection('subtract');
|
||||
}
|
||||
const spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
|
||||
return cm.replaceSelection(spaces);
|
||||
},
|
||||
'Ctrl-Enter': function() {
|
||||
executeChallenge();
|
||||
return false;
|
||||
},
|
||||
'Cmd-Enter': function() {
|
||||
executeChallenge();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
componentDidMount() {
|
||||
const { updateFile = (() => {}) } = this.props;
|
||||
this._subscription = this._editorContent$
|
||||
.debounce(editorDebounceTimeout)
|
||||
.distinctUntilChanged()
|
||||
.subscribe(
|
||||
updateFile,
|
||||
err => { throw err; }
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._subscription) {
|
||||
this._subscription.dispose();
|
||||
this._subscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(value) {
|
||||
if (this._subscription) {
|
||||
this._editorContent$.onNext(value);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { executeChallenge, content, height, mode } = this.props;
|
||||
const style = {};
|
||||
if (height) {
|
||||
style.height = height + 'px';
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className='challenges-editor'
|
||||
style={ style }
|
||||
>
|
||||
<NoSSR>
|
||||
<Codemirror
|
||||
onChange={ this.handleChange }
|
||||
options={ this.createOptions({ executeChallenge, mode, options }) }
|
||||
value={ content }
|
||||
/>
|
||||
</NoSSR>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Editor);
|
32
common/app/routes/challenges/components/classic/Output.jsx
Normal file
32
common/app/routes/challenges/components/classic/Output.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import NoSSR from 'react-no-ssr';
|
||||
import Codemirror from 'react-codemirror';
|
||||
|
||||
const defaultOptions = {
|
||||
lineNumbers: false,
|
||||
mode: 'javascript',
|
||||
theme: 'monokai',
|
||||
readOnly: 'nocursor',
|
||||
lineWrapping: true
|
||||
};
|
||||
|
||||
export default class extends PureComponent {
|
||||
static displayName = 'Output';
|
||||
static propTypes = {
|
||||
output: PropTypes.string
|
||||
};
|
||||
render() {
|
||||
const { output } = this.props;
|
||||
return (
|
||||
<div className='challenge-log'>
|
||||
<NoSSR>
|
||||
<Codemirror
|
||||
options={ defaultOptions }
|
||||
value={ output }
|
||||
/>
|
||||
</NoSSR>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
25
common/app/routes/challenges/components/classic/Preview.jsx
Normal file
25
common/app/routes/challenges/components/classic/Preview.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
|
||||
const mainId = 'fcc-main-frame';
|
||||
export default class extends PureComponent {
|
||||
static displayName = 'Preview';
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className='hidden-xs hidden-md'>
|
||||
<img
|
||||
className='iphone-position iframe-scroll'
|
||||
src='https://s3.amazonaws.com/freecodecamp/iphone6-frame.png'
|
||||
/>
|
||||
</div>
|
||||
<iframe
|
||||
className='iphone iframe-scroll'
|
||||
id={ mainId }
|
||||
/>
|
||||
<div className='spacer' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
156
common/app/routes/challenges/components/classic/Side-Panel.jsx
Normal file
156
common/app/routes/challenges/components/classic/Side-Panel.jsx
Normal file
@ -0,0 +1,156 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDom from 'react-dom';
|
||||
import { createSelector } from 'reselect';
|
||||
import { connect } from 'react-redux';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { Col, Row } from 'react-bootstrap';
|
||||
|
||||
import TestSuite from './Test-Suite.jsx';
|
||||
import Output from './Output.jsx';
|
||||
import ToolPanel from './Tool-Panel.jsx';
|
||||
import { challengeSelector } from '../../redux/selectors';
|
||||
import {
|
||||
openBugModal,
|
||||
updateHint,
|
||||
executeChallenge
|
||||
} from '../../redux/actions';
|
||||
import { makeToast } from '../../../../toasts/redux/actions';
|
||||
import { toggleHelpChat } from '../../../../redux/actions';
|
||||
|
||||
const bindableActions = {
|
||||
makeToast,
|
||||
executeChallenge,
|
||||
updateHint,
|
||||
toggleHelpChat,
|
||||
openBugModal
|
||||
};
|
||||
const mapStateToProps = createSelector(
|
||||
challengeSelector,
|
||||
state => state.app.windowHeight,
|
||||
state => state.app.navHeight,
|
||||
state => state.challengesApp.tests,
|
||||
state => state.challengesApp.output,
|
||||
state => state.challengesApp.hintIndex,
|
||||
(
|
||||
{ challenge: { title, description, hints = [] } = {} },
|
||||
windowHeight,
|
||||
navHeight,
|
||||
tests,
|
||||
output,
|
||||
hintIndex
|
||||
) => ({
|
||||
title,
|
||||
description,
|
||||
height: windowHeight - navHeight - 20,
|
||||
tests,
|
||||
output,
|
||||
hint: hints[hintIndex]
|
||||
})
|
||||
);
|
||||
|
||||
export class SidePanel extends PureComponent {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.descriptionRegex = /\<blockquote|\<ol|\<h4|\<table/;
|
||||
}
|
||||
static displayName = 'SidePanel';
|
||||
|
||||
static propTypes = {
|
||||
description: PropTypes.arrayOf(PropTypes.string),
|
||||
height: PropTypes.number,
|
||||
tests: PropTypes.arrayOf(PropTypes.object),
|
||||
title: PropTypes.string,
|
||||
output: PropTypes.string,
|
||||
hints: PropTypes.string,
|
||||
updateHint: PropTypes.func,
|
||||
makeToast: PropTypes.func,
|
||||
toggleHelpChat: PropTypes.func,
|
||||
openBugModal: PropTypes.func
|
||||
};
|
||||
|
||||
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
|
||||
return description.map((line, index) => {
|
||||
if (descriptionRegex.test(line)) {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: line }}
|
||||
key={ line.slice(-6) + index }
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p
|
||||
className='wrappable'
|
||||
dangerouslySetInnerHTML= {{ __html: line }}
|
||||
key={ line.slice(-6) + index }
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.title !== nextProps.title) {
|
||||
ReactDom.findDOMNode(this).scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
height,
|
||||
tests = [],
|
||||
output,
|
||||
hint,
|
||||
executeChallenge,
|
||||
updateHint,
|
||||
makeToast,
|
||||
toggleHelpChat,
|
||||
openBugModal
|
||||
} = this.props;
|
||||
const style = {
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
if (height) {
|
||||
style.height = height + 'px';
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref='panel'
|
||||
style={ style }
|
||||
>
|
||||
<div>
|
||||
<h4 className='text-center challenge-instructions-title'>
|
||||
{ title || 'Happy Coding!' }
|
||||
</h4>
|
||||
<hr />
|
||||
<Row>
|
||||
<Col
|
||||
className='challenge-instructions'
|
||||
xs={ 12 }
|
||||
>
|
||||
{ this.renderDescription(description, this.descriptionRegex) }
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<ToolPanel
|
||||
executeChallenge={ executeChallenge }
|
||||
hint={ hint }
|
||||
makeToast={ makeToast }
|
||||
openBugModal={ openBugModal }
|
||||
toggleHelpChat={ toggleHelpChat }
|
||||
updateHint={ updateHint }
|
||||
/>
|
||||
<Output output={ output }/>
|
||||
<br />
|
||||
<TestSuite tests={ tests } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
bindableActions
|
||||
)(SidePanel);
|
@ -0,0 +1,54 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { Col, Row } from 'react-bootstrap';
|
||||
|
||||
export default class extends PureComponent {
|
||||
static displayName = 'TestSuite';
|
||||
static proptTypes = {
|
||||
tests: PropTypes.arrayOf(PropTypes.object)
|
||||
};
|
||||
|
||||
renderTests(tests = []) {
|
||||
// err && pass > invalid state
|
||||
// err && !pass > failed tests
|
||||
// !err && pass > passed tests
|
||||
// !err && !pass > in-progress
|
||||
return tests.map(({ err, pass = false, text = '' }, index)=> {
|
||||
const iconClass = classnames({
|
||||
'big-icon': true,
|
||||
'ion-close-circled error-icon': err && !pass,
|
||||
'ion-checkmark-circled success-icon': !err && pass,
|
||||
'ion-refresh refresh-icon': !err && !pass
|
||||
});
|
||||
return (
|
||||
<Row key={ text.slice(-6) + index }>
|
||||
<Col
|
||||
className='text-center'
|
||||
xs={ 2 }
|
||||
>
|
||||
<i className={ iconClass } />
|
||||
</Col>
|
||||
<Col
|
||||
className='test-output'
|
||||
dangerouslySetInnerHTML={{ __html: text }}
|
||||
xs={ 10 }
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tests } = this.props;
|
||||
return (
|
||||
<div
|
||||
className='challenge-test-suite'
|
||||
style={{ marginTop: '10px' }}
|
||||
>
|
||||
{ this.renderTests(tests) }
|
||||
<div className='big-spacer' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
106
common/app/routes/challenges/components/classic/Tool-Panel.jsx
Normal file
106
common/app/routes/challenges/components/classic/Tool-Panel.jsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Button, ButtonGroup } from 'react-bootstrap';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
|
||||
export default class ToolPanel extends PureComponent {
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
this.makeHint = this.makeHint.bind(this);
|
||||
this.makeReset = this.makeReset.bind(this);
|
||||
}
|
||||
static displayName = 'ToolPanel';
|
||||
|
||||
static propTypes = {
|
||||
executeChallenge: PropTypes.func,
|
||||
updateHint: PropTypes.func,
|
||||
hint: PropTypes.string,
|
||||
toggleHelpChat: PropTypes.func,
|
||||
openBugModal: PropTypes.func
|
||||
};
|
||||
|
||||
makeHint() {
|
||||
this.props.makeToast({
|
||||
message: this.props.hint,
|
||||
timeout: 4000
|
||||
});
|
||||
this.props.updateHint();
|
||||
}
|
||||
|
||||
makeReset() {
|
||||
this.props.makeToast({
|
||||
message: 'This will restore your code editor to its original state.',
|
||||
action: 'clear my code',
|
||||
actionCreator: 'resetChallenge',
|
||||
timeout: 4000
|
||||
});
|
||||
}
|
||||
|
||||
renderHint(hint, makeHint) {
|
||||
if (!hint) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
block={ true }
|
||||
bsStyle='primary'
|
||||
className='btn-big'
|
||||
onClick={ makeHint }
|
||||
>
|
||||
Hint
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
hint,
|
||||
executeChallenge,
|
||||
toggleHelpChat,
|
||||
openBugModal
|
||||
} = this.props;
|
||||
return (
|
||||
<div>
|
||||
{ this.renderHint(hint, this.makeHint) }
|
||||
<Button
|
||||
block={ true }
|
||||
bsStyle='primary'
|
||||
className='btn-big'
|
||||
onClick={ executeChallenge }
|
||||
>
|
||||
Run tests (ctrl + enter)
|
||||
</Button>
|
||||
<div className='button-spacer' />
|
||||
<ButtonGroup
|
||||
className='input-group'
|
||||
justified={ true }
|
||||
>
|
||||
<Button
|
||||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
componentClass='label'
|
||||
onClick={ this.makeReset }
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
componentClass='label'
|
||||
onClick={ toggleHelpChat }
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
<Button
|
||||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
componentClass='label'
|
||||
onClick={ openBugModal }
|
||||
>
|
||||
Bug
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<div className='button-spacer' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
111
common/app/routes/challenges/components/map/Block.jsx
Normal file
111
common/app/routes/challenges/components/map/Block.jsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FA from 'react-fontawesome';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { Panel } from 'react-bootstrap';
|
||||
|
||||
import Challenge from './Challenge.jsx';
|
||||
import { toggleThisPanel } from '../../redux/actions';
|
||||
import {
|
||||
makePanelOpenSelector,
|
||||
makePanelHiddenSelector
|
||||
} from '../../redux/selectors';
|
||||
|
||||
const dispatchActions = { toggleThisPanel };
|
||||
const makeMapStateToProps = () => createSelector(
|
||||
(_, props) => props.dashedName,
|
||||
(state, props) => state.entities.block[props.dashedName],
|
||||
makePanelOpenSelector(),
|
||||
makePanelHiddenSelector(),
|
||||
(dashedName, block, isOpen, isHidden) => {
|
||||
return {
|
||||
isOpen,
|
||||
isHidden,
|
||||
dashedName,
|
||||
title: block.title,
|
||||
time: block.time,
|
||||
challenges: block.challenges
|
||||
};
|
||||
}
|
||||
);
|
||||
export class Block extends PureComponent {
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
}
|
||||
static displayName = 'Block';
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
dashedName: PropTypes.string,
|
||||
time: PropTypes.string,
|
||||
isOpen: PropTypes.bool,
|
||||
isHidden: PropTypes.bool,
|
||||
challenges: PropTypes.array,
|
||||
toggleThisPanel: PropTypes.func
|
||||
};
|
||||
|
||||
handleSelect(eventKey, e) {
|
||||
e.preventDefault();
|
||||
this.props.toggleThisPanel(eventKey);
|
||||
}
|
||||
|
||||
renderHeader(isOpen, title, time, isCompleted) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className={ isCompleted ? 'faded clear-fix' : 'clear-fix' }>
|
||||
<FA
|
||||
className='no-link-underline'
|
||||
name={ isOpen ? 'caret-down' : 'caret-right' }
|
||||
/>
|
||||
<span>
|
||||
{ title }
|
||||
</span>
|
||||
<span className='challenge-block-time'>({ time })</span>
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderChallenges(challenges) {
|
||||
if (!Array.isArray(challenges) || !challenges.length) {
|
||||
return <div>No Challenges Found</div>;
|
||||
}
|
||||
return challenges.map(dashedName => (
|
||||
<Challenge
|
||||
dashedName={ dashedName }
|
||||
key={ dashedName }
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
time,
|
||||
dashedName,
|
||||
isOpen,
|
||||
isHidden,
|
||||
challenges
|
||||
} = this.props;
|
||||
if (isHidden) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Panel
|
||||
bsClass='map-accordion-panel-nested'
|
||||
collapsible={ true }
|
||||
eventKey={ dashedName || title }
|
||||
expanded={ isOpen }
|
||||
header={ this.renderHeader(isOpen, title, time) }
|
||||
id={ title }
|
||||
key={ title }
|
||||
onSelect={ this.handleSelect }
|
||||
>
|
||||
{ this.renderChallenges(challenges) }
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, dispatchActions)(Block);
|
145
common/app/routes/challenges/components/map/Challenge.jsx
Normal file
145
common/app/routes/challenges/components/map/Challenge.jsx
Normal file
@ -0,0 +1,145 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Link } from 'react-router';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import classnames from 'classnames';
|
||||
import debug from 'debug';
|
||||
|
||||
import { updateCurrentChallenge } from '../../redux/actions';
|
||||
import { makePanelHiddenSelector } from '../../redux/selectors';
|
||||
|
||||
const bindableActions = { updateCurrentChallenge };
|
||||
const makeMapStateToProps = () => createSelector(
|
||||
(_, props) => props.dashedName,
|
||||
state => state.entities.challenge,
|
||||
makePanelHiddenSelector(),
|
||||
(dashedName, challengeMap, isHidden) => {
|
||||
const challenge = challengeMap[dashedName] || {};
|
||||
return {
|
||||
dashedName,
|
||||
challenge,
|
||||
isHidden,
|
||||
title: challenge.title,
|
||||
block: challenge.block,
|
||||
isLocked: challenge.isLocked,
|
||||
isRequired: challenge.isRequired,
|
||||
isCompleted: challenge.isCompleted,
|
||||
isComingSoon: challenge.isComingSoon,
|
||||
isDev: debug.enabled('fcc:*')
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export class Challenge extends PureComponent {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
static displayName = 'Challenge';
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
dashedName: PropTypes.string,
|
||||
block: PropTypes.string,
|
||||
isLocked: PropTypes.bool,
|
||||
isRequired: PropTypes.bool,
|
||||
isCompleted: PropTypes.bool,
|
||||
isHidden: PropTypes.bool,
|
||||
challenge: PropTypes.object,
|
||||
updateCurrentChallenge: PropTypes.func
|
||||
};
|
||||
|
||||
renderCompleted(isCompleted, isLocked) {
|
||||
if (isLocked || !isCompleted) {
|
||||
return null;
|
||||
}
|
||||
return <span className='sr-only'>completed</span>;
|
||||
}
|
||||
|
||||
renderRequired(isRequired) {
|
||||
if (!isRequired) {
|
||||
return '';
|
||||
}
|
||||
return <span className='text-primary'><strong>*</strong></span>;
|
||||
}
|
||||
|
||||
renderComingSoon(isComingSoon) {
|
||||
if (!isComingSoon) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className='text-info small'>
|
||||
   
|
||||
<strong>
|
||||
<em>Coming Soon</em>
|
||||
</strong>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderLocked(title, isRequired, isComingSoon, className) {
|
||||
return (
|
||||
<p
|
||||
className={ className }
|
||||
key={ title }
|
||||
>
|
||||
{ title }
|
||||
{ this.renderRequired(isRequired) }
|
||||
{ this.renderComingSoon(isComingSoon) }
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
dashedName,
|
||||
block,
|
||||
isLocked,
|
||||
isRequired,
|
||||
isCompleted,
|
||||
isComingSoon,
|
||||
isDev,
|
||||
isHidden,
|
||||
challenge,
|
||||
updateCurrentChallenge
|
||||
} = this.props;
|
||||
if (isHidden) {
|
||||
return null;
|
||||
}
|
||||
const challengeClassName = classnames({
|
||||
'text-primary': true,
|
||||
'padded-ionic-icon': true,
|
||||
'negative-15': true,
|
||||
'challenge-title': true,
|
||||
'ion-checkmark-circled faded': !(isLocked || isComingSoon) && isCompleted,
|
||||
'ion-ios-circle-outline': !(isLocked || isComingSoon) && !isCompleted,
|
||||
'ion-locked': isLocked || isComingSoon,
|
||||
disabled: isLocked || (!isDev && isComingSoon)
|
||||
});
|
||||
if (isLocked || (!isDev && isComingSoon)) {
|
||||
return this.renderLocked(
|
||||
title,
|
||||
isRequired,
|
||||
isComingSoon,
|
||||
challengeClassName
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p
|
||||
className={ challengeClassName }
|
||||
key={ title }
|
||||
>
|
||||
<Link to={ `/challenges/${block}/${dashedName}` }>
|
||||
<span onClick={ () => updateCurrentChallenge(challenge) }>
|
||||
{ title }
|
||||
{ this.renderCompleted(isCompleted, isLocked) }
|
||||
{ this.renderRequired(isRequired) }
|
||||
</span>
|
||||
</Link>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, bindableActions)(Challenge);
|
118
common/app/routes/challenges/components/map/Header.jsx
Normal file
118
common/app/routes/challenges/components/map/Header.jsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PureComponent from 'react-pure-render/component';
|
||||
import { InputGroup, FormControl, Button, Row } from 'react-bootstrap';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
clearFilter,
|
||||
updateFilter,
|
||||
collapseAll,
|
||||
expandAll
|
||||
} from '../../redux/actions';
|
||||
|
||||
const ESC = 27;
|
||||
const clearIcon = <i className='fa fa-times' />;
|
||||
const searchIcon = <i className='fa fa-search' />;
|
||||
const bindableActions = {
|
||||
clearFilter,
|
||||
updateFilter,
|
||||
collapseAll,
|
||||
expandAll
|
||||
};
|
||||
const mapStateToProps = state => ({
|
||||
isAllCollapsed: state.challengesApp.mapUi.isAllCollapsed,
|
||||
filter: state.challengesApp.filter
|
||||
});
|
||||
export class Header extends PureComponent {
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleClearButton = this.handleClearButton.bind(this);
|
||||
}
|
||||
static displayName = 'MapHeader';
|
||||
static propTypes = {
|
||||
isAllCollapsed: PropTypes.bool,
|
||||
filter: PropTypes.string,
|
||||
clearFilter: PropTypes.func,
|
||||
updateFilter: PropTypes.func,
|
||||
collapseAll: PropTypes.func,
|
||||
expandAll: PropTypes.func
|
||||
};
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (e.keyCode === ESC) {
|
||||
this.props.clearFilter();
|
||||
}
|
||||
}
|
||||
|
||||
handleClearButton(e) {
|
||||
e.preventDefault();
|
||||
this.props.clearFilter();
|
||||
}
|
||||
|
||||
renderSearchAddon(filter) {
|
||||
if (!filter) {
|
||||
return searchIcon;
|
||||
}
|
||||
return <span onClick={this.handleClearButton }>{ clearIcon }</span>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
filter,
|
||||
updateFilter,
|
||||
collapseAll,
|
||||
expandAll,
|
||||
isAllCollapsed
|
||||
} = this.props;
|
||||
const inputClass = classnames({
|
||||
'map-filter': true,
|
||||
filled: !!filter
|
||||
});
|
||||
const buttonClass = classnames({
|
||||
'center-block': true,
|
||||
active: isAllCollapsed
|
||||
});
|
||||
const buttonCopy = isAllCollapsed ?
|
||||
'Expand all challenges' :
|
||||
'Collapse all challenges';
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className='text-center map-fixed-header'
|
||||
style={{ top: '50px' }}
|
||||
>
|
||||
<p>Challenges required for certifications are marked with a *</p>
|
||||
<Row className='map-buttons'>
|
||||
<Button
|
||||
block={ true }
|
||||
bsStyle='primary'
|
||||
className={ buttonClass }
|
||||
onClick={ isAllCollapsed ? expandAll : collapseAll }
|
||||
>
|
||||
{ buttonCopy }
|
||||
</Button>
|
||||
</Row>
|
||||
<Row className='map-buttons'>
|
||||
<InputGroup>
|
||||
<FormControl
|
||||
autocompleted='off'
|
||||
className={ inputClass }
|
||||
onChange={ updateFilter }
|
||||
onKeyDown={ this.handleKeyDown }
|
||||
placeholder='Type a challenge name'
|
||||
type='text'
|
||||
value={ filter }
|
||||
/>
|
||||
<InputGroup.Addon>
|
||||
{ this.renderSearchAddon(filter) }
|
||||
</InputGroup.Addon>
|
||||
</InputGroup>
|
||||
</Row>
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default connect(mapStateToProps, bindableActions)(Header);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user