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 ( +
+
+ +  Blog   + + +  Twitch   + + +  Github   + + +  Twitter   + + +  Facebook   + + +  About   + + +  Privacy   + +
+
+ + + Free Code Camp\'s Blog + + + + + Free Code Camp Live Pair Programming on Twitch.tv + + + + + Free Code Camp on GitHub + + + + + Free Code Camp on Twitter + + + + + Free Code Camp on Facebook + + + + + About Free Code Camp + + + + + Free Code Camp's Privacy Policy + + +
+
+ ); + } +}); + +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 ( + + learn to code javascript at Free Code Camp logo + + ); + }, + + _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');