Merge pull request #7430 from FreeCodeCamp/feature/react-challenges

Feature(app): Reactify FCC!
This commit is contained in:
Quincy Larson
2016-07-29 00:19:48 -07:00
committed by GitHub
278 changed files with 23046 additions and 14088 deletions

View File

@ -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
View File

@ -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
View 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
View 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);
});

View File

@ -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);
});

View File

@ -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; },

View File

@ -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 {

View File

@ -1,3 +1,4 @@
.chat-embed-help-title,
.chat-embed-main-title {
display: flex;
flex-grow: 1;

View 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
View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -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
}

View File

@ -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";

View File

@ -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 {

View File

@ -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;
}

View File

@ -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';

View File

@ -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 });

View 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);
});
}));
}

View 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);
}));
}

View 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);
}

View 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;
});
}

View File

@ -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'
}
});
};
};
}));
}

View 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
View 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
View 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;
})
);
}

View File

@ -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;
});
}

View File

@ -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
];

View File

@ -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);
};
};

View 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());
}

View File

@ -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;
});
}

View 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);
}

View File

@ -0,0 +1,6 @@
export default function sendPageAnalytics(history, ga) {
history.listen(location => {
ga('set', 'page', location.pathname + location.search);
ga('send', 'pageview');
});
}

View 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(); }
};
};
}

View File

@ -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);

View 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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -1 +0,0 @@
Currently not used

View File

@ -1 +0,0 @@
export default from './Footer.jsx';

View File

@ -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"
}
]

View 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>
);
}
}

View 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 };

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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);

View File

@ -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
}));
}

View File

@ -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 })
});
}

View File

@ -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);

View 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;
}

View File

@ -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);
});
}

View File

@ -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 ];

View File

@ -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
);

View 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] || {}
})
);

View File

@ -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');

View File

@ -1 +0,0 @@
This folder contains things relative to the bonfires' screens

View File

@ -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);

View File

@ -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);

View File

@ -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>
);
}
});

View File

@ -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
}]
};

View File

@ -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);

View File

@ -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;
};
};

View File

@ -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);
};
};

View File

@ -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 ];

View File

@ -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
);

View File

@ -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] || {})
);

View File

@ -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');

View File

@ -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];
}

View File

@ -1 +0,0 @@
This folder contains everything relative to Jobs board

View File

@ -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>
);
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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>
);
}
}

View File

@ -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);

View File

@ -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 }>
Well review your listing and email you when its 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>
);
}
}

View File

@ -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);

View File

@ -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 Camps Front ' +
'End Development Certification in order to apply';
}
if (isBackEndCertReq && !isBackEndCert) {
return 'This employer requires Free Code Camps 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);

View File

@ -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>
);
}
});

View File

@ -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
}]
};

View File

@ -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);

View File

@ -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);
};
};

View File

@ -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);
};
};

View File

@ -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 ];

View File

@ -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)
}
};

View File

@ -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
);

View File

@ -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);
};
};

View File

@ -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');

View File

@ -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;
}

View 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);

View 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);

View 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);

View 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);

View 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>
);
}
}

View 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>
);
}
}

View 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);

View File

@ -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>
);
}
}

View 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>
);
}
}

View 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);

View 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'>
&thinsp; &thinsp;
<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);

View 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