diff --git a/common/screens/App.jsx b/common/screens/App.jsx
new file mode 100644
index 0000000000..a0756abc14
--- /dev/null
+++ b/common/screens/App.jsx
@@ -0,0 +1,19 @@
+var React = require('react'),
+ RouteHandler = require('react-router').RouteHandler,
+
+ // ## components
+ Nav = require('./nav'),
+ Footer = require('./footer');
+
+var App = React.createClass({
+ render: function() {
+ return (
+
+
+
+
+
+ );
+ }
+});
+module.exports = App;
diff --git a/common/screens/Router.jsx b/common/screens/Router.jsx
new file mode 100644
index 0000000000..650c4107ad
--- /dev/null
+++ b/common/screens/Router.jsx
@@ -0,0 +1,34 @@
+var React = require('react'),
+
+ // react router
+ Router = require('react-router'),
+ Route = Router.Route,
+ // NotFound = Router.NotFoundRoute,
+ DefaultRoute = Router.DefaultRoute,
+
+ // # Components
+ App = require('./App.jsx'),
+ Bonfires = require('./bonfires');
+
+var routes = (
+
+
+
+
+
+
+);
+
+module.exports = function(Location) {
+ return Router.create({
+ routes: routes,
+ location: Location
+ });
+};
diff --git a/common/screens/bonfires/Actions.js b/common/screens/bonfires/Actions.js
new file mode 100644
index 0000000000..1930b48890
--- /dev/null
+++ b/common/screens/bonfires/Actions.js
@@ -0,0 +1,63 @@
+var Action = require('thundercats').Action,
+ executeBonfire = require('./executeBonfire'),
+ getModel = require('../../utils/getModel'),
+ debug = require('debug')('freecc:common:bonfires');
+
+var BonfireActions = Action.createActions([
+ 'setUserCode',
+ 'testUserCode',
+ 'setResults',
+ 'setDisplay',
+ 'setBonfire',
+ 'getBonfire',
+ 'handleBonfireError',
+ 'openCompletionModal'
+]);
+
+BonfireActions
+ .getBonfire
+ .subscribe(function(params) {
+ var Bonfire = getModel('bonfire');
+ var bonfireName = params.bonfireName ?
+ params.bonfireName.replace(/\-/g, ' ') :
+ 'meet bonfire';
+ debug('getting bonfire for: ', bonfireName);
+ var regQuery = { name: { like: bonfireName, options: 'i' } };
+ Bonfire.find(
+ { where: regQuery },
+ function(err, bonfire) {
+ if (err) {
+ return debug('bonfire get err', err);
+ }
+ if (!bonfire || bonfire.length < 1) {
+ return debug('404 no bonfire found for ', bonfireName);
+ }
+ bonfire = bonfire.pop();
+ if (bonfire) {
+ debug(
+ 'found bonfire %s for route %s',
+ bonfire.name,
+ bonfireName
+ );
+ }
+ BonfireActions.setBonfire(bonfire);
+ }
+ );
+ });
+
+
+BonfireActions
+ .testUserCode
+ .subscribe(function({ userCode, tests }) {
+ debug('test bonfire');
+ executeBonfire(userCode, tests, function(err, { output, results }) {
+ if (err) {
+ debug('error running tests', err);
+ return BonfireActions.setDisplay(err);
+ }
+ BonfireActions.setDisplay(output);
+ BonfireActions.setResults(results);
+ });
+ });
+
+module.exports = BonfireActions;
diff --git a/common/screens/bonfires/Bonfires.jsx b/common/screens/bonfires/Bonfires.jsx
new file mode 100644
index 0000000000..9da6db9320
--- /dev/null
+++ b/common/screens/bonfires/Bonfires.jsx
@@ -0,0 +1,99 @@
+var React = require('react'),
+
+ // ## mixins
+ { ObservableStateMixin } = require('thundercats'),
+
+ // ## components
+ SidePanel = require('./SidePanel.jsx'),
+ Results = require('./Results.jsx'),
+ Display = require('../displayCode'),
+ Editor = require('../editor'),
+ { Grid, Row, Col } = require('react-bootstrap'),
+
+ // ## flux
+ BonfireActions = require('./Actions'),
+ BonfireStore = require('./Store');
+
+var Bonfire = React.createClass({
+
+ mixins: [ObservableStateMixin],
+
+ contextTypes: {
+ makePath: React.PropTypes.func.isRequired,
+ replaceWith: React.PropTypes.func.isRequired
+ },
+
+ getObservable: function() {
+ return BonfireStore;
+ },
+
+ componentDidMount: function() {
+ // get history object
+ var his = typeof window !== 'undefined' ? window.history : null;
+ // spinal-case bonfireName
+ var bonfireName = this.state.name.toLowerCase().replace(/\s/g, '-');
+ // create proper URI from react-router
+ var path = this.context.makePath('bonfires', { bonfireName: bonfireName });
+
+ // if html5 push state exists, update URI
+ // else we are using hash location and should just cause a re render
+ if (his) {
+ his.replaceState({ path: path }, '', path);
+ } else {
+ this.context.replaceWith('bonfires', { bonfireName: bonfireName});
+ }
+ },
+
+ _onTestBonfire: function() {
+ BonfireActions.testUserCode({
+ userCode: this.state.userCode,
+ tests: this.state.tests
+ });
+ },
+
+ render: function() {
+ var {
+ name,
+ userCode,
+ difficulty,
+ description,
+ results,
+ display
+ } = this.state;
+ var brief = description.slice(0, 1).pop();
+
+ // convert bonfire difficulty from floating point string
+ // to integer.
+ var difficultyInt = Math.floor(+difficulty);
+
+ return (
+
+
+
+ 1 ? description : [] }/>
+
+
+
+
+
+
+
+
+ );
+ }
+});
+
+module.exports = Bonfire;
diff --git a/common/screens/bonfires/Results.jsx b/common/screens/bonfires/Results.jsx
new file mode 100644
index 0000000000..8e0e11b449
--- /dev/null
+++ b/common/screens/bonfires/Results.jsx
@@ -0,0 +1,62 @@
+var React = require('react'),
+ classNames = require('classnames'),
+ { Grid, Row, Col } = require('react-bootstrap');
+
+var Results = React.createClass({
+
+ propTypes: {
+ results: React.PropTypes.array
+ },
+
+ _renderText: function(text, textClass) {
+ return (
+
+ { text }
+
+ );
+ },
+
+ _renderResult: function(results) {
+ return results.map(function(result, idx) {
+ var err = result.err;
+ var iconClass = {
+ 'ion-close-circled big-error-icon': err,
+ 'ion-checkmark-circled big-success-icon': !err
+ };
+ var textClass = {
+ 'test-output wrappable': true,
+ 'test-vertical-center': !err
+ };
+ return (
+
+
+
+
+
+ { this._renderText(result.text, textClass) }
+ { err ? this._renderText(err, textClass) : null }
+
+
+
+ );
+ }.bind(this));
+ },
+
+ render: function() {
+ var results = this.props.results;
+ if (!results || results.length && results.length === 0) {
+ return null;
+ }
+ return (
+
+ { this._renderResult(this.props.results) }
+
+ );
+ }
+});
+
+module.exports = Results;
diff --git a/common/screens/bonfires/SidePanel.jsx b/common/screens/bonfires/SidePanel.jsx
new file mode 100644
index 0000000000..078e9fe72b
--- /dev/null
+++ b/common/screens/bonfires/SidePanel.jsx
@@ -0,0 +1,129 @@
+var React = require('react'),
+
+ // ## components
+ {
+ Well,
+ Row,
+ Col,
+ Button,
+ } = require('react-bootstrap');
+
+var SidePanel = React.createClass({
+
+ propTypes: {
+ name: React.PropTypes.string,
+ brief: React.PropTypes.string,
+ description: React.PropTypes.array,
+ difficulty: React.PropTypes.number,
+ onTestBonfire: React.PropTypes.func
+ },
+
+ getDefaultProps: function() {
+ return {
+ name: 'Welcome to Bonfires!',
+ difficulty: 5,
+ brief: 'This is a brief description'
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ isMoreInfoOpen: false
+ };
+ },
+
+ _toggleMoreInfo: function() {
+ this.setState({
+ isMoreInfoOpen: !this.state.isMoreInfoOpen
+ });
+ },
+
+ _renderFlames: function() {
+ var difficulty = this.props.difficulty;
+
+ return [1, 2, 3, 4, 5].map(num => {
+ var className = 'ion-ios-flame';
+ if (num > difficulty) {
+ className += '-outline';
+ }
+ return (
+
+ );
+ });
+ },
+
+ _renderMoreInfo: function(isDescription) {
+ var description = this.props.description.map((sentance, index) => {
+ return { sentance }
;
+ });
+
+ if (isDescription && this.state.isMoreInfoOpen) {
+ return (
+
+
+ { description }
+
+
+ );
+ }
+ return null;
+ },
+
+ _renderMoreInfoButton: function(isDescription) {
+ if (isDescription) {
+ return (
+
+ );
+ }
+ return null;
+ },
+
+ render: function() {
+ var isDescription = this.props.description &&
+ this.props.description.length > 1;
+
+ return (
+
+
{ this.props.name }
+
+
+ Difficulty:
+ { this._renderFlames() }
+
+
+
+
+
+
+
{ this.props.brief }
+
+ { this._renderMoreInfo(isDescription) }
+ { this._renderMoreInfoButton(isDescription) }
+
+
+
+
+
+
+
+
+ );
+ }
+});
+
+module.exports = SidePanel;
diff --git a/common/screens/bonfires/Store.js b/common/screens/bonfires/Store.js
new file mode 100644
index 0000000000..4a83c223d6
--- /dev/null
+++ b/common/screens/bonfires/Store.js
@@ -0,0 +1,67 @@
+var BonfiresActions = require('./Actions');
+var { Store, setStateUtil } = require('thundercats');
+
+var BonfiresStore = Store.create({
+
+ getInitialValue: function() {
+ return {
+ userCode: 'console.log(\'FreeCodeCamp!\')',
+ difficulty: 0,
+ description: [
+ 'default state'
+ ],
+ tests: [],
+ results: null
+ };
+ },
+
+ getOperations: function() {
+ var {
+ setBonfire,
+ setUserCode,
+ setResults,
+ setDisplay
+ } = BonfiresActions;
+
+ return [
+ setBonfire
+ .map(function(bonfire) {
+ var {
+ name,
+ description,
+ difficulty,
+ tests
+ } = bonfire;
+ var userCode = bonfire.challengeSeed;
+ return {
+ name,
+ userCode,
+ tests,
+ description,
+ difficulty
+ };
+ })
+ .map(setStateUtil),
+
+ setUserCode
+ .map(function(userCode) {
+ return { userCode };
+ })
+ .map(setStateUtil),
+
+ setDisplay
+ .map(function(display) {
+ return { display };
+ })
+ .map(setStateUtil),
+
+ setResults
+ .map(function(results) {
+ return { results };
+ })
+ .map(setStateUtil)
+ ];
+ }
+});
+
+module.exports = BonfiresStore;
diff --git a/common/screens/bonfires/executeBonfire.js b/common/screens/bonfires/executeBonfire.js
new file mode 100644
index 0000000000..0d1795894a
--- /dev/null
+++ b/common/screens/bonfires/executeBonfire.js
@@ -0,0 +1,27 @@
+var debug = require('debug')('freecc:executebonfire');
+var {
+ addTests,
+ runTests,
+ testCode
+} = require('../../utils');
+
+module.exports = executeBonfire;
+
+function executeBonfire(userCode, tests, cb) {
+
+ // TODO: move this into componentDidMount
+ // ga('send', 'event', 'Bonfire', 'ran-code', bonfireName);
+ var testSalt = Math.random();
+ var { preppedCode, userTests } = addTests(userCode, tests, testSalt);
+
+ debug('sending code to web worker for testing');
+ testCode(preppedCode, function(err, data) {
+ if (err) { return cb(err); }
+ var results = runTests(userTests, data, testSalt);
+ debug('testing complete', results);
+ cb(null, {
+ output: data.output,
+ results
+ });
+ });
+}
diff --git a/common/screens/bonfires/index.js b/common/screens/bonfires/index.js
new file mode 100644
index 0000000000..5557fd9f16
--- /dev/null
+++ b/common/screens/bonfires/index.js
@@ -0,0 +1 @@
+module.exports = require('./Bonfires.jsx');
diff --git a/common/screens/context/Actions.js b/common/screens/context/Actions.js
new file mode 100644
index 0000000000..83e06c38e1
--- /dev/null
+++ b/common/screens/context/Actions.js
@@ -0,0 +1,34 @@
+var debug = require('debug')('freecc:context'),
+ BonfireActions = require('../bonfires/Actions'),
+ BonfireStore = require('../bonfires/Store');
+
+var {
+ Action,
+ waitFor
+} = require('thundercats');
+
+var actions = Action.createActions([
+ 'setContext',
+ 'renderToUser'
+]);
+
+actions
+ .setContext
+ .filter(function(ctx) {
+ return ctx.state.path.indexOf('/bonfire') !== -1;
+ })
+ .subscribe(function(ctx) {
+ debug('set ctx');
+ BonfireActions.getBonfire(ctx.state.params);
+ waitFor(BonfireStore)
+ .firstOrDefault()
+ .catch(function(err) {
+ // handle timeout error
+ debug('err', err);
+ })
+ .subscribe(function() {
+ actions.renderToUser(ctx);
+ });
+ });
+
+module.exports = actions;
diff --git a/common/screens/context/Store.js b/common/screens/context/Store.js
new file mode 100644
index 0000000000..f94308ace1
--- /dev/null
+++ b/common/screens/context/Store.js
@@ -0,0 +1,18 @@
+var Store = require('thundercats').Store,
+ ContextActions = require('./Actions');
+
+var ContextStore = Store.create({
+ getInitialValue: function() {
+ return {};
+ },
+
+ getOperations: function() {
+ return ContextActions
+ .renderToUser
+ .map(function(ctx) {
+ return { value: ctx };
+ });
+ }
+});
+
+module.exports = ContextStore;
diff --git a/common/screens/displayCode/Display.jsx b/common/screens/displayCode/Display.jsx
new file mode 100644
index 0000000000..9eb60db8da
--- /dev/null
+++ b/common/screens/displayCode/Display.jsx
@@ -0,0 +1,51 @@
+var React = require('react'),
+ Tailspin = require('tailspin');
+
+var Editor = React.createClass({
+
+ propTypes: {
+ value: React.PropTypes.string
+ },
+
+ getDefaultProps: function() {
+ return {
+ value: [
+ '/**',
+ '* Your output will go here.',
+ '* Console.log() -type statements',
+ '* will appear in your browser\'s',
+ '* DevTools JavaScript console.',
+ '**/'
+ ].join('\n')
+ };
+ },
+
+ render: function() {
+ var value = this.props.value;
+ var options = {
+ lineNumbers: false,
+ lineWrapping: true,
+ mode: 'text',
+ readOnly: 'noCursor',
+ textAreaClassName: 'hide-textarea',
+ theme: 'monokai',
+ value: value
+ };
+
+ var config = {
+ setSize: ['100%', '100%']
+ };
+
+ return (
+
+ );
+ }
+});
+
+module.exports = Editor;
diff --git a/common/screens/displayCode/index.js b/common/screens/displayCode/index.js
new file mode 100644
index 0000000000..d3e3560174
--- /dev/null
+++ b/common/screens/displayCode/index.js
@@ -0,0 +1 @@
+module.exports = require('./Display.jsx');
diff --git a/common/screens/editor/Editor.jsx b/common/screens/editor/Editor.jsx
new file mode 100644
index 0000000000..8e72f5d4a0
--- /dev/null
+++ b/common/screens/editor/Editor.jsx
@@ -0,0 +1,91 @@
+var React = require('react'),
+ debug = require('debug')('freecc:comp:editor'),
+ jshint = require('jshint').JSHINT,
+ Tailspin = require('tailspin');
+
+var Editor = React.createClass({
+
+ propTypes: {
+ onValueChange: React.PropTypes.func,
+ value: React.PropTypes.string
+ },
+
+ getDefaultProps: function() {
+ return {
+ value: 'console.log(\'freeCodeCamp is awesome\')'
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ value: this.props.value
+ };
+ },
+
+ render: function() {
+ var options = {
+ autoCloseBrackets: true,
+ gutters: ['CodeMirror-lint-markers'],
+ lint: true,
+ linter: jshint,
+ lineNumbers: true,
+ lineWrapping: true,
+ mode: 'javascript',
+ matchBrackets: true,
+ runnable: true,
+ scrollbarStyle: 'null',
+ theme: 'monokai',
+ textAreaClassName: 'hide-textarea',
+ value: this.state.value,
+ onChange: e => {
+ this.setState({ value: e.target.value});
+ if (typeof this.props.onValueChange === 'function') {
+ this.props.onValueChange(e.target.value);
+ }
+ }
+ };
+
+ var config = {
+ setSize: ['100%', 'auto'],
+ extraKeys: {
+ Tab: function(cm) {
+ debug('tab pressed');
+ if (cm.somethingSelected()) {
+ cm.indentSelection('add');
+ } else {
+ var spaces = new Array(cm.getOption('indentUnit') + 1).join(' ');
+ cm.replaceSelection(spaces);
+ }
+ },
+ 'Shift-Tab': function(cm) {
+ debug('shift-tab pressed');
+ if (cm.somethingSelected()) {
+ cm.indentSelection('subtract');
+ } else {
+ var spaces = new Array(cm.getOption('indentUnit') + 1).join(' ');
+ cm.replaceSelection(spaces);
+ }
+ },
+ 'Ctrl-Enter': function() {
+ debug('C-enter pressed');
+ // execute bonfire action
+ return false;
+ }
+ }
+ };
+
+ return (
+
+ );
+ }
+});
+
+module.exports = Editor;
diff --git a/common/screens/editor/index.js b/common/screens/editor/index.js
new file mode 100644
index 0000000000..5e431fabff
--- /dev/null
+++ b/common/screens/editor/index.js
@@ -0,0 +1 @@
+module.exports = require('./Editor.jsx');
diff --git a/common/screens/footer/Footer.jsx b/common/screens/footer/Footer.jsx
new file mode 100644
index 0000000000..5dd5e1b296
--- /dev/null
+++ b/common/screens/footer/Footer.jsx
@@ -0,0 +1,106 @@
+var React = require('react');
+
+var Footer = React.createClass({
+ render: function() {
+ return (
+
+ );
+ }
+});
+
+module.exports = Footer;
diff --git a/common/screens/footer/index.js b/common/screens/footer/index.js
new file mode 100644
index 0000000000..286ef5f0fb
--- /dev/null
+++ b/common/screens/footer/index.js
@@ -0,0 +1 @@
+module.exports = require('./Footer.jsx');
diff --git a/common/screens/nav/Nav.jsx b/common/screens/nav/Nav.jsx
new file mode 100644
index 0000000000..52a9a8728b
--- /dev/null
+++ b/common/screens/nav/Nav.jsx
@@ -0,0 +1,81 @@
+var React = require('react'),
+ bootStrap = require('react-bootstrap'),
+ Navbar = bootStrap.Navbar,
+ Nav = bootStrap.Nav,
+ NavItem = bootStrap.NavItem,
+ NavItemFCC = require('./NavItem.jsx');
+
+var NavBarComp = React.createClass({
+
+ propTypes: { signedIn: React.PropTypes.bool },
+
+ getDefaultProps: function() {
+ return { signedIn: false };
+ },
+
+ _renderBrand: function() {
+ var fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
+ return (
+
+
+
+ );
+ },
+
+ _renderSignin: function() {
+ if (this.props.signedIn) {
+ return (
+
+ Show Picture
+
+ );
+ } else {
+ return (
+
+ Sign In
+
+ );
+ }
+ },
+
+ render: function() {
+
+ return (
+
+
+
+ );
+ }
+});
+module.exports = NavBarComp;
diff --git a/common/screens/nav/NavItem.jsx b/common/screens/nav/NavItem.jsx
new file mode 100644
index 0000000000..88c4f363a9
--- /dev/null
+++ b/common/screens/nav/NavItem.jsx
@@ -0,0 +1,66 @@
+var React = require('react/addons');
+var joinClasses = require('react-bootstrap/lib/utils/joinClasses');
+var classSet = React.addons.classSet;
+var BootstrapMixin = require('react-bootstrap').BootstrapMixin;
+
+var NavItem = React.createClass({
+ mixins: [BootstrapMixin],
+
+ propTypes: {
+ onSelect: React.PropTypes.func,
+ active: React.PropTypes.bool,
+ disabled: React.PropTypes.bool,
+ href: React.PropTypes.string,
+ title: React.PropTypes.string,
+ eventKey: React.PropTypes.any,
+ target: React.PropTypes.string
+ },
+
+ getDefaultProps: function () {
+ return {
+ href: '#'
+ };
+ },
+
+ render: function () {
+ var {
+ disabled,
+ active,
+ href,
+ title,
+ target,
+ children,
+ } = this.props,
+ props = this.props,
+ classes = {
+ 'active': active,
+ 'disabled': disabled
+ };
+
+ return (
+
+
+ { children }
+
+
+ );
+ },
+
+ handleClick: function (e) {
+ if (this.props.onSelect) {
+ e.preventDefault();
+
+ if (!this.props.disabled) {
+ this.props.onSelect(this.props.eventKey, this.props.href, this.props.target);
+ }
+ }
+ }
+});
+
+module.exports = NavItem;
diff --git a/common/screens/nav/index.js b/common/screens/nav/index.js
new file mode 100644
index 0000000000..cdf087548a
--- /dev/null
+++ b/common/screens/nav/index.js
@@ -0,0 +1 @@
+module.exports = require('./Nav.jsx');