Permalink
Please sign in to comment.
Showing
with
1,243 additions
and 0 deletions.
- +3 −0 .babelrc
- +257 −0 .eslintrc
- +6 −0 .gitignore
- BIN Challenge_Editor_Mockup.png
- BIN Layout.png
- +17 −0 README.md
- +75 −0 app.js
- +94 −0 bin/www
- +9 −0 config.js
- +10 −0 index.html
- +37 −0 package.json
- +10 −0 src/App.js
- +156 −0 src/Components/Editor.js
- +228 −0 src/Components/GrandCentralStation.js
- +74 −0 src/Components/Menu.js
- +44 −0 src/Components/SelectChallenge.js
- +49 −0 src/Components/Tabs.js
- +48 −0 src/actions/editorActions.js
- +11 −0 src/index.js
- +73 −0 src/reducers/reducer.js
- +6 −0 src/store/store.js
- 0 src/style.css
- +3 −0 views/error.jade
- +1 −0 views/index.jade
- +7 −0 views/layout.jade
- +25 −0 webpack.config.js
3
.babelrc
@@ -0,0 +1,3 @@ | ||
+{ | ||
+ "presets": ["es2015", "react"] | ||
+} |
257
.eslintrc
@@ -0,0 +1,257 @@ | ||
+{ | ||
+ "ecmaFeatures": { | ||
+ "jsx": true | ||
+ }, | ||
+ "env": { | ||
+ "browser": true, | ||
+ "mocha": true, | ||
+ "node": true | ||
+ }, | ||
+ "parser": "babel-eslint", | ||
+ "plugins": [ | ||
+ "react" | ||
+ ], | ||
+ "globals": { | ||
+ "window": true, | ||
+ "$": true, | ||
+ "ga": true, | ||
+ "jQuery": true, | ||
+ "router": true | ||
+ }, | ||
+ "rules": { | ||
+ "comma-dangle": 2, | ||
+ "no-cond-assign": 2, | ||
+ "no-console": 0, | ||
+ "no-constant-condition": 2, | ||
+ "no-control-regex": 2, | ||
+ "no-debugger": 2, | ||
+ "no-dupe-keys": 2, | ||
+ "no-empty": 2, | ||
+ "no-empty-character-class": 2, | ||
+ "no-ex-assign": 2, | ||
+ "no-extra-boolean-cast": 2, | ||
+ "no-extra-parens": 0, | ||
+ "no-extra-semi": 2, | ||
+ "no-func-assign": 2, | ||
+ "no-inner-declarations": 2, | ||
+ "no-invalid-regexp": 2, | ||
+ "no-irregular-whitespace": 2, | ||
+ "no-negated-in-lhs": 2, | ||
+ "no-obj-calls": 2, | ||
+ "no-regex-spaces": 2, | ||
+ "no-reserved-keys": 0, | ||
+ "no-sparse-arrays": 2, | ||
+ "no-unreachable": 2, | ||
+ "use-isnan": 2, | ||
+ "valid-jsdoc": 2, | ||
+ "valid-typeof": 2, | ||
+ "block-scoped-var": 0, | ||
+ "complexity": 0, | ||
+ "consistent-return": 2, | ||
+ "curly": 2, | ||
+ "default-case": 1, | ||
+ "dot-notation": 0, | ||
+ "eqeqeq": 1, | ||
+ "guard-for-in": 1, | ||
+ "no-alert": 1, | ||
+ "no-caller": 2, | ||
+ "no-div-regex": 2, | ||
+ "no-else-return": 0, | ||
+ "no-empty-label": 2, | ||
+ "no-eq-null": 1, | ||
+ "no-eval": 2, | ||
+ "no-extend-native": 2, | ||
+ "no-extra-bind": 2, | ||
+ "no-fallthrough": 2, | ||
+ "no-floating-decimal": 2, | ||
+ "no-implied-eval": 2, | ||
+ "no-iterator": 2, | ||
+ "no-labels": 2, | ||
+ "no-lone-blocks": 2, | ||
+ "no-loop-func": 1, | ||
+ "no-multi-spaces": 1, | ||
+ "no-multi-str": 2, | ||
+ "no-native-reassign": 2, | ||
+ "no-new": 2, | ||
+ "no-new-func": 2, | ||
+ "no-new-wrappers": 2, | ||
+ "no-octal": 2, | ||
+ "no-octal-escape": 2, | ||
+ "no-process-env": 0, | ||
+ "no-proto": 2, | ||
+ "no-redeclare": 1, | ||
+ "no-return-assign": 2, | ||
+ "no-script-url": 2, | ||
+ "no-self-compare": 2, | ||
+ "no-sequences": 2, | ||
+ "no-unused-expressions": 2, | ||
+ "no-void": 1, | ||
+ "no-warning-comments": [ | ||
+ 1, | ||
+ { | ||
+ "terms": [ | ||
+ "fixme" | ||
+ ], | ||
+ "location": "start" | ||
+ } | ||
+ ], | ||
+ "no-with": 2, | ||
+ "radix": 2, | ||
+ "vars-on-top": 0, | ||
+ "wrap-iife": [ | ||
+ 2, | ||
+ "any" | ||
+ ], | ||
+ "yoda": 0, | ||
+ "strict": 0, | ||
+ "no-catch-shadow": 2, | ||
+ "no-delete-var": 2, | ||
+ "no-label-var": 2, | ||
+ "no-shadow": 0, | ||
+ "no-shadow-restricted-names": 2, | ||
+ "no-undef": 2, | ||
+ "no-undef-init": 2, | ||
+ "no-undefined": 1, | ||
+ "no-unused-vars": 2, | ||
+ "no-use-before-define": 0, | ||
+ "handle-callback-err": 2, | ||
+ "no-mixed-requires": 0, | ||
+ "no-new-require": 2, | ||
+ "no-path-concat": 2, | ||
+ "no-process-exit": 2, | ||
+ "no-restricted-modules": 0, | ||
+ "no-sync": 0, | ||
+ "brace-style": [ | ||
+ 2, | ||
+ "1tbs", | ||
+ { | ||
+ "allowSingleLine": true | ||
+ } | ||
+ ], | ||
+ "camelcase": 1, | ||
+ "comma-spacing": [ | ||
+ 2, | ||
+ { | ||
+ "before": false, | ||
+ "after": true | ||
+ } | ||
+ ], | ||
+ "comma-style": [ | ||
+ 2, | ||
+ "last" | ||
+ ], | ||
+ "consistent-this": 0, | ||
+ "eol-last": 2, | ||
+ "func-names": 0, | ||
+ "func-style": 0, | ||
+ "key-spacing": [ | ||
+ 2, | ||
+ { | ||
+ "beforeColon": false, | ||
+ "afterColon": true | ||
+ } | ||
+ ], | ||
+ "max-nested-callbacks": 0, | ||
+ "new-cap": 0, | ||
+ "new-parens": 2, | ||
+ "no-array-constructor": 2, | ||
+ "no-inline-comments": 1, | ||
+ "no-lonely-if": 1, | ||
+ "no-mixed-spaces-and-tabs": 2, | ||
+ "no-multiple-empty-lines": [ | ||
+ 1, | ||
+ { | ||
+ "max": 2 | ||
+ } | ||
+ ], | ||
+ "no-nested-ternary": 2, | ||
+ "no-new-object": 2, | ||
+ "semi-spacing": [ | ||
+ 2, | ||
+ { | ||
+ "before": false, | ||
+ "after": true | ||
+ } | ||
+ ], | ||
+ "no-spaced-func": 2, | ||
+ "no-ternary": 0, | ||
+ "no-trailing-spaces": 1, | ||
+ "no-underscore-dangle": 0, | ||
+ "one-var": 0, | ||
+ "operator-assignment": 0, | ||
+ "padded-blocks": 0, | ||
+ "quote-props": 0, | ||
+ "quotes": [ | ||
+ 2, | ||
+ "single", | ||
+ "avoid-escape" | ||
+ ], | ||
+ "semi": [ | ||
+ 2, | ||
+ "always" | ||
+ ], | ||
+ "sort-vars": 0, | ||
+ "space-after-keywords": [ | ||
+ 2, | ||
+ "always" | ||
+ ], | ||
+ "space-before-function-paren": [ | ||
+ 2, | ||
+ "never" | ||
+ ], | ||
+ "space-before-blocks": [ | ||
+ 2, | ||
+ "always" | ||
+ ], | ||
+ "space-in-brackets": 0, | ||
+ "space-in-parens": 0, | ||
+ "space-infix-ops": 2, | ||
+ "space-return-throw-case": 2, | ||
+ "space-unary-ops": [ | ||
+ 1, | ||
+ { | ||
+ "words": true, | ||
+ "nonwords": false | ||
+ } | ||
+ ], | ||
+ "spaced-comment": [ | ||
+ 2, | ||
+ "always", | ||
+ { | ||
+ "exceptions": [ | ||
+ "-" | ||
+ ] | ||
+ } | ||
+ ], | ||
+ "wrap-regex": 1, | ||
+ "max-depth": 0, | ||
+ "max-len": [ | ||
+ 1, | ||
+ 80, | ||
+ 2 | ||
+ ], | ||
+ "max-params": 0, | ||
+ "max-statements": 0, | ||
+ "no-bitwise": 1, | ||
+ "no-plusplus": 0, | ||
+ "react/jsx-boolean-value": [ | ||
+ 1, | ||
+ "always" | ||
+ ], | ||
+ "jsx-quotes": [ | ||
+ 1, | ||
+ "prefer-single" | ||
+ ], | ||
+ "react/jsx-no-undef": 1, | ||
+ "react/jsx-sort-props": 1, | ||
+ "react/jsx-uses-react": 1, | ||
+ "react/jsx-uses-vars": 1, | ||
+ "react/no-did-mount-set-state": 2, | ||
+ "react/no-did-update-set-state": 2, | ||
+ "react/no-multi-comp": 2, | ||
+ "react/prop-types": 2, | ||
+ "react/react-in-jsx-scope": 1, | ||
+ "react/self-closing-comp": 1, | ||
+ "react/wrap-multilines": 1 | ||
+ } | ||
+} |
6
.gitignore
@@ -0,0 +1,6 @@ | ||
+node_modules | ||
+npm-debug.log | ||
+.DS_Store | ||
+dist | ||
+.idea | ||
+.env |
BIN
Challenge_Editor_Mockup.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN
Layout.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,17 @@ | ||
+# Challenge-O-Matic 1003 | ||
+Free Code Camp's Challenge Editor (Very Alpha) | ||
+ | ||
+## Note | ||
+This **will** alter the challenge files in your Free Code Camp `/seed/challenges` directory! | ||
+ | ||
+ | ||
+## Initial Setup | ||
+* Clone this repo to the same directory where your Free Code Camp directory is (ex> `/Users/terakilobyte/Developer)` | ||
+* `cd ChallengeOMatic1003 && npm install` | ||
+* `<yourFCC_FOLDER_NAME>` is whatever you named your Free Code Camp repo locally(mine is freecodecamp). | ||
+* `echo 'FCC_FOLDER_NAME = "<yourFCC_FOLDER_NAME>"' >> .env` | ||
+* `npm run build` | ||
+ | ||
+## To Run | ||
+* `npm start` | ||
+ |
75
app.js
@@ -0,0 +1,75 @@ | ||
+var express = require('express'); | ||
+var path = require('path'); | ||
+var logger = require('morgan'); | ||
+var bodyParser = require('body-parser'); | ||
+var fs = require('fs'); | ||
+var config = require('./config'); | ||
+var ObjectID = require('mongodb').ObjectID; | ||
+ | ||
+var app = express(); | ||
+ | ||
+// view engine setup | ||
+app.set('views', path.join(__dirname, 'views')); | ||
+app.set('view engine', 'jade'); | ||
+ | ||
+// uncomment after placing your favicon in /public | ||
+// app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); | ||
+app.use(logger('dev')); | ||
+app.use(bodyParser.urlencoded({parameterLimit: 10000000, limit: '50mb', extended: true})); | ||
+app.use(bodyParser.json({limit: '50mb'})); | ||
+app.use(express.static(path.join(__dirname, 'public'))); | ||
+ | ||
+app.post('/export', function(req, res, next) { | ||
+ Object.keys(req.body.data).forEach(function(file) { | ||
+ var fileData = req.body.data[file]; | ||
+ console.log(JSON.stringify(fileData, null, 2)); | ||
+ fs.writeFile(config.fccPath + file, | ||
+ JSON.stringify(fileData, null, 2), | ||
+ function(err) { | ||
+ console.error(err); | ||
+ }); | ||
+ }); | ||
+}); | ||
+ | ||
+app.get('/mongoid', function(req, res, next) { | ||
+ var objectId = new ObjectID(); | ||
+ res.json({objectId: objectId}); | ||
+}); | ||
+ | ||
+app.get('/*', function(req, res, next) { | ||
+ res.render('index'); | ||
+}); | ||
+ | ||
+// catch 404 and forward to error handler | ||
+app.use(function(req, res, next) { | ||
+ var err = new Error('Not Found'); | ||
+ err.status = 404; | ||
+ next(err); | ||
+}); | ||
+ | ||
+// error handlers | ||
+ | ||
+// development error handler | ||
+// will print stacktrace | ||
+if (app.get('env') === 'development') { | ||
+ app.use(function(err, req, res, next) { | ||
+ res.status(err.status || 500); | ||
+ res.render('error', { | ||
+ message: err.message, | ||
+ error: err | ||
+ }); | ||
+ }); | ||
+} | ||
+ | ||
+// production error handler | ||
+// no stacktraces leaked to user | ||
+app.use(function(err, req, res, next) { | ||
+ res.status(err.status || 500); | ||
+ res.render('error', { | ||
+ message: err.message, | ||
+ error: {} | ||
+ }); | ||
+}); | ||
+ | ||
+ | ||
+module.exports = app; |
94
bin/www
@@ -0,0 +1,94 @@ | ||
+#!/usr/bin/env node | ||
+ | ||
+/** | ||
+ * Module dependencies. | ||
+ */ | ||
+ | ||
+var app = require('../app'); | ||
+var debug = require('debug')('ChallengeOMatic1003:server'); | ||
+var http = require('http'); | ||
+var dotenv = require('dotenv'); | ||
+ | ||
+/** | ||
+ * Get port from environment and store in Express. | ||
+ */ | ||
+ | ||
+dotenv.load(); | ||
+ | ||
+var port = normalizePort(process.env.PORT || '3000'); | ||
+app.set('port', port); | ||
+ | ||
+/** | ||
+ * Create HTTP server. | ||
+ */ | ||
+ | ||
+var server = http.createServer(app); | ||
+ | ||
+/** | ||
+ * Listen on provided port, on all network interfaces. | ||
+ */ | ||
+ | ||
+server.listen(port); | ||
+server.on('error', onError); | ||
+server.on('listening', onListening); | ||
+ | ||
+/** | ||
+ * Normalize a port into a number, string, or false. | ||
+ */ | ||
+ | ||
+function normalizePort(val) { | ||
+ var port = parseInt(val, 10); | ||
+ | ||
+ if (isNaN(port)) { | ||
+ // named pipe | ||
+ return val; | ||
+ } | ||
+ | ||
+ if (port >= 0) { | ||
+ // port number | ||
+ return port; | ||
+ } | ||
+ | ||
+ return false; | ||
+} | ||
+ | ||
+/** | ||
+ * Event listener for HTTP server "error" event. | ||
+ */ | ||
+ | ||
+function onError(error) { | ||
+ if (error.syscall !== 'listen') { | ||
+ throw error; | ||
+ } | ||
+ | ||
+ var bind = typeof port === 'string' | ||
+ ? 'Pipe ' + port | ||
+ : 'Port ' + port; | ||
+ | ||
+ // handle specific listen errors with friendly messages | ||
+ switch (error.code) { | ||
+ case 'EACCES': | ||
+ console.error(bind + ' requires elevated privileges'); | ||
+ process.exit(1); | ||
+ break; | ||
+ case 'EADDRINUSE': | ||
+ console.error(bind + ' is already in use'); | ||
+ process.exit(1); | ||
+ break; | ||
+ default: | ||
+ throw error; | ||
+ } | ||
+} | ||
+ | ||
+/** | ||
+ * Event listener for HTTP server "listening" event. | ||
+ */ | ||
+ | ||
+function onListening() { | ||
+ var addr = server.address(); | ||
+ var bind = typeof addr === 'string' | ||
+ ? 'pipe ' + addr | ||
+ : 'port ' + addr.port; | ||
+ debug('Listening on ' + bind); | ||
+ console.log('listening on port: ' + addr.port); | ||
+} |
9
config.js
@@ -0,0 +1,9 @@ | ||
+var dotenv = require('dotenv'); | ||
+dotenv.load(); | ||
+ | ||
+var PATH_MARKER = process.platform === 'win32' ? '\\' : '/'; | ||
+module.exports = { | ||
+ 'fccPath': process.cwd().substr(0, process.cwd().lastIndexOf(PATH_MARKER)) | ||
+ + '/' + process.env.FCC_FOLDER_NAME + '/seed/challenges/' | ||
+}; | ||
+ |
10
index.html
@@ -0,0 +1,10 @@ | ||
+<html> | ||
+ <head> | ||
+ <title>Sample App</title> | ||
+ </head> | ||
+ <body> | ||
+ <div id='root'> | ||
+ </div> | ||
+ <script src="/static/bundle.js"></script> | ||
+ </body> | ||
+</html> |
37
package.json
@@ -0,0 +1,37 @@ | ||
+{ | ||
+ "name": "ChallengeOMatic1003", | ||
+ "version": "0.0.0", | ||
+ "private": true, | ||
+ "scripts": { | ||
+ "build": "webpack", | ||
+ "start": "node ./bin/www" | ||
+ }, | ||
+ "dependencies": { | ||
+ "babel-core": "^6.2.1", | ||
+ "body-parser": "^1.14.1", | ||
+ "codemirror": "^5.8.0", | ||
+ "cookie-parser": "~1.3.5", | ||
+ "debug": "~2.2.0", | ||
+ "dotenv": "^1.2.0", | ||
+ "express": "^4.13.3", | ||
+ "jade": "~1.11.0", | ||
+ "jquery": "^2.1.4", | ||
+ "material-ui": "^0.13.3", | ||
+ "mongodb": "^2.0.49", | ||
+ "morgan": "~1.6.1", | ||
+ "react": "^0.14.3", | ||
+ "react-dom": "^0.14.3", | ||
+ "react-redux": "^4.0.0", | ||
+ "react-tap-event-plugin": "^0.2.1", | ||
+ "redux": "^3.0.4", | ||
+ "style": "0.0.3" | ||
+ }, | ||
+ "devDependencies": { | ||
+ "babel-loader": "^6.2.0", | ||
+ "babel-preset-es2015": "^6.1.18", | ||
+ "babel-preset-react": "^6.1.18", | ||
+ "css-loader": "^0.23.0", | ||
+ "style-loader": "^0.13.0", | ||
+ "webpack": "^1.12.8" | ||
+ } | ||
+} |
10
src/App.js
@@ -0,0 +1,10 @@ | ||
+import React, { Component } from 'react'; | ||
+import GrandCentralStation from './Components/GrandCentralStation'; | ||
+ | ||
+export default class App extends Component { | ||
+ render() { | ||
+ return ( | ||
+ <GrandCentralStation /> | ||
+ ); | ||
+ } | ||
+} |
156
src/Components/Editor.js
@@ -0,0 +1,156 @@ | ||
+import './../../node_modules/codemirror/lib/codemirror.css'; | ||
+import './../../node_modules/codemirror/theme/monokai.css'; | ||
+import './../../node_modules/codemirror/addon/scroll/simplescrollbars.css'; | ||
+ | ||
+import React, {Component} from 'react'; | ||
+ | ||
+import {updateChallenge} from './../actions/editorActions'; | ||
+ | ||
+import {connect} from 'react-redux'; | ||
+ | ||
+import CodeMirror from './../../node_modules/codemirror/lib/codemirror'; | ||
+import './../../node_modules/codemirror/mode/javascript/javascript'; | ||
+import './../../node_modules/codemirror/mode/xml/xml'; | ||
+import './../../node_modules/codemirror/mode/css/css'; | ||
+import './../../node_modules/codemirror/mode/htmlmixed/htmlmixed'; | ||
+import './../../node_modules/codemirror/addon/edit/closebrackets'; | ||
+import './../../node_modules/codemirror/addon/edit/matchbrackets'; | ||
+import './../../node_modules/codemirror/addon/scroll/simplescrollbars'; | ||
+import './../../node_modules/codemirror/addon/scroll/annotatescrollbar'; | ||
+import './../../node_modules/codemirror/addon/scroll/scrollpastend'; | ||
+import './../../node_modules/codemirror/addon/lint/lint'; | ||
+import './../../node_modules/codemirror/addon/lint/javascript-lint'; | ||
+ | ||
+const connector = connect(function(state, props) { | ||
+ // State from redux | ||
+ return ( | ||
+ { | ||
+ challenge: state.challenges.reduce(function(prevC, challenge) { | ||
+ if (challenge.id === props.id) { | ||
+ return (challenge); | ||
+ } else { | ||
+ return (prevC); | ||
+ } | ||
+ }, {}), | ||
+ activeFile: state.activeFile | ||
+ } | ||
+ ); | ||
+}); | ||
+ | ||
+class Editor extends Component { | ||
+ | ||
+ constructor(props) { | ||
+ super(props); | ||
+ | ||
+ var codeMirrorData = []; | ||
+ | ||
+ var unrenderedCodeMirrors = []; | ||
+ | ||
+ for (let i in this.props.challenge ) { | ||
+ var challengeDataField = this.props.challenge[i]; | ||
+ codeMirrorData.push([i, challengeDataField]); | ||
+ } | ||
+ | ||
+ codeMirrorData = codeMirrorData.filter(function(field) { | ||
+ return (field[0] !== 'id'); | ||
+ }); | ||
+ | ||
+ unrenderedCodeMirrors = codeMirrorData.map(function(data) { | ||
+ if (Array.isArray(data[1])) { | ||
+ if (data[0] === 'tests') { | ||
+ data[1] = data[1].join('EOL\n'); | ||
+ } else { | ||
+ data[1] = data[1].join('\n'); | ||
+ } | ||
+ } | ||
+ return ( | ||
+ <div key = {data[0]}> | ||
+ <h3>{data[0]}</h3> | ||
+ <textarea id = {data[0]} defaultValue = {data[1]}></textarea> | ||
+ </div> | ||
+ ); | ||
+ }); | ||
+ | ||
+ this.state = { | ||
+ codeMirrorData: codeMirrorData, | ||
+ unrenderedCodeMirrors: unrenderedCodeMirrors | ||
+ }; | ||
+ } | ||
+ | ||
+ componentDidMount() { | ||
+ let codeMirrors = []; | ||
+ const dispatch = this.props.dispatch; | ||
+ const challengeId = this.props.challenge.id; | ||
+ const activeFile = this.props.activeFile; | ||
+ const challengeType = this.props.challenge.challengeType; | ||
+ | ||
+ this.state.codeMirrorData.map(function(codeMirror) { | ||
+ // Determine mode | ||
+ let mode = 'htmlmixed'; | ||
+ /* eslint-disable no-fallthrough */ | ||
+ switch (codeMirror[0]) { | ||
+ case 'challengeSeed': | ||
+ case 'solutions': | ||
+ if (challengeType !== 5 && challengeType !== 1) { | ||
+ mode = 'htmlmixed'; | ||
+ break; | ||
+ } | ||
+ case 'head': | ||
+ case 'tail': | ||
+ case 'tests': | ||
+ mode = 'javascript'; | ||
+ break; | ||
+ default: | ||
+ mode = 'htmlmixed'; | ||
+ break; | ||
+ } | ||
+ /* eslint-enable no-fallthrough */ | ||
+ | ||
+ let editor = CodeMirror.fromTextArea( | ||
+ document.getElementById(codeMirror[0]), | ||
+ { | ||
+ lineNumbers: true, | ||
+ mode: mode, | ||
+ theme: 'monokai', | ||
+ runnable: true, | ||
+ matchBrackets: true, | ||
+ autoCloseBrackets: true, | ||
+ scrollbarStyle: 'simple', | ||
+ lineWrapping: true, | ||
+ gutters: ['CodeMirror-lint-markers'] | ||
+ } | ||
+ ); | ||
+ | ||
+ editor.on('change', function(instance) { | ||
+ updateChallenge(dispatch, | ||
+ { | ||
+ id: challengeId, | ||
+ props: { | ||
+ [codeMirror[0]]: instance.getValue() | ||
+ }, | ||
+ activeFile: activeFile | ||
+ | ||
+ } | ||
+ ); | ||
+ }); | ||
+ | ||
+ codeMirrors.push(editor); | ||
+ }); | ||
+ } | ||
+ | ||
+ render() { | ||
+ return ( | ||
+ <div> | ||
+ {this.state.unrenderedCodeMirrors} | ||
+ </div> | ||
+ ); | ||
+ } | ||
+} | ||
+ | ||
+export default connector(Editor); | ||
+ | ||
+Editor.propTypes = { | ||
+ challenge: React.PropTypes.object.isRequired, | ||
+ activeFile: React.PropTypes.string.isRequired, | ||
+ dispatch: React.PropTypes.func.isRequired | ||
+}; |
228
src/Components/GrandCentralStation.js
@@ -0,0 +1,228 @@ | ||
+import React, { Component } from 'react'; | ||
+import {connect} from 'react-redux'; | ||
+import { | ||
+ backAction, | ||
+ loadFile, | ||
+ createChallenge, | ||
+ loadChallenge, | ||
+ fileSelect | ||
+} from './../actions/editorActions'; | ||
+ | ||
+import $ from 'jquery'; | ||
+ | ||
+import Menu from './Menu'; | ||
+import TabBar from './Tabs'; | ||
+import SelectChallenge from './SelectChallenge'; | ||
+import Editor from './Editor'; | ||
+ | ||
+import './../style.css'; | ||
+ | ||
+const connector = connect(function(state) { | ||
+ return ( | ||
+ state | ||
+ ); | ||
+}, null, null, {pure: false}); | ||
+ | ||
+class GrandCentralStation extends Component { | ||
+ | ||
+ constructor(props) { | ||
+ super(props); | ||
+ this.backView = this.backView.bind(this); | ||
+ this.exportFiles = this.exportFiles.bind(this); | ||
+ this.handleFileSelect = this.handleFileSelect.bind(this); | ||
+ this.handleFileIsSelected = this.handleFileIsSelected.bind(this); | ||
+ this.handleChallengeClick = this.handleChallengeClick.bind(this); | ||
+ } | ||
+ | ||
+ backView() { | ||
+ let dispatch = this.props.dispatch; | ||
+ backAction(dispatch, { | ||
+ view: 'challengeSelect' | ||
+ }); | ||
+ } | ||
+ | ||
+ exportFiles() { | ||
+ $.post('/export', { | ||
+ data: this.props.fileStore, | ||
+ success: function(data) { | ||
+ console.log(data); | ||
+ } | ||
+ }); | ||
+ } | ||
+ | ||
+ handleFileSelect(to) { | ||
+ let dispatch = this.props.dispatch; | ||
+ let fileStore = this.props.fileStore; | ||
+ fileSelect(dispatch, { | ||
+ activeFile: to, | ||
+ challenges: fileStore[to].challenges | ||
+ }); | ||
+ } | ||
+ | ||
+ handleFileIsSelected(event) { | ||
+ | ||
+ let files = event.target.files; | ||
+ for (let i in files) { | ||
+ if (files.hasOwnProperty(i)) { | ||
+ let file = files[i]; | ||
+ let reader = new FileReader(); | ||
+ let dispatch = this.props.dispatch; | ||
+ | ||
+ reader.onload = function(upload) { | ||
+ let newFileStoreObject = this.props.fileStore; | ||
+ newFileStoreObject[file.name] = | ||
+ JSON.parse(upload.target.result); | ||
+ | ||
+ loadFile(dispatch, { | ||
+ fileStore: newFileStoreObject, | ||
+ activeFile: file.name, | ||
+ challenges: newFileStoreObject[file.name].challenges, | ||
+ activeChallenge: {} | ||
+ }); | ||
+ }.bind(this); | ||
+ reader.readAsText(file); | ||
+ } | ||
+ } | ||
+ } | ||
+ | ||
+ handleChallengeClick(id) { | ||
+ let dispatch = this.props.dispatch; | ||
+ | ||
+ let oldFileStore = this.props.fileStore; | ||
+ let currentFile = this.props.activeFile; | ||
+ | ||
+ if (id === 'new') { | ||
+ $.getJSON('/mongoid', function(mongoid) { | ||
+ mongoid = mongoid.objectId; | ||
+ oldFileStore[currentFile].challenges.push({ | ||
+ 'id': mongoid, | ||
+ 'title': mongoid, | ||
+ 'description': [ | ||
+ '' | ||
+ ], | ||
+ 'tests': [ | ||
+ '' | ||
+ ], | ||
+ 'challengeSeed': [ | ||
+ '' | ||
+ ], | ||
+ 'MDNlinks': [ | ||
+ '' | ||
+ ], | ||
+ 'solutions': [ | ||
+ '' | ||
+ ], | ||
+ 'type': '', | ||
+ 'challengeType': 0, | ||
+ 'nameCn': '', | ||
+ 'descriptionCn': [], | ||
+ 'nameFr': '', | ||
+ 'descriptionFr': [], | ||
+ 'nameRu': '', | ||
+ 'descriptionRu': [], | ||
+ 'nameEs': '', | ||
+ 'descriptionEs': [], | ||
+ 'namePt': '', | ||
+ 'descriptionPt': [] | ||
+ }); | ||
+ | ||
+ let AddedChallenge = {fileStore: oldFileStore}; | ||
+ | ||
+ createChallenge(dispatch, | ||
+ AddedChallenge | ||
+ ); | ||
+ }); | ||
+ } else { | ||
+ loadChallenge(dispatch, { | ||
+ 'activeChallenge': | ||
+ this.props.fileStore[this.props.activeFile] | ||
+ .challenges.filter((challenge) => { | ||
+ return challenge.id === id; | ||
+ }).pop(), 'view': 'ChallengeEdit' | ||
+ }); | ||
+ } | ||
+ } | ||
+ | ||
+ render() { | ||
+ let elements = []; | ||
+ let selectChallenges; | ||
+ if (this.props !== null && this.props.view === 'ChallengeSelect') { | ||
+ elements = [ | ||
+ { | ||
+ name: 'Choose File', | ||
+ handleChange: this.handleFileIsSelected | ||
+ }, | ||
+ { | ||
+ name: 'Export', | ||
+ action: this.exportFiles | ||
+ } | ||
+ ]; | ||
+ } else { | ||
+ elements = [ | ||
+ { | ||
+ name: 'Choose File', | ||
+ handleChange: this.handleFileIsSelected | ||
+ }, | ||
+ { | ||
+ name: 'Export', | ||
+ action: this.exportFiles | ||
+ }, | ||
+ { | ||
+ name: 'Back', | ||
+ action: this.backView | ||
+ } | ||
+ ]; | ||
+ } | ||
+ | ||
+ if (this.props !== null | ||
+ && this.props.fileStore | ||
+ && Object.keys(this.props.fileStore).length) { | ||
+ selectChallenges = ( | ||
+ <SelectChallenge | ||
+ challengeClick = {this.handleChallengeClick} | ||
+ data = {this.props.fileStore[this.props.activeFile]} | ||
+ /> | ||
+ ); | ||
+ } | ||
+ | ||
+ let menu = | ||
+ <Menu elements = {elements} />; | ||
+ | ||
+ let tabs; | ||
+ | ||
+ tabs = Object.keys(this.props.fileStore).length === 0 ? '' | ||
+ : <TabBar action = {this.handleFileSelect} | ||
+ files = {this.props.fileStore} />; | ||
+ | ||
+ if (Object.keys(this.props.view === 'ChallengeEdit' && | ||
+ this.props.activeChallenge).length) { | ||
+ return ( | ||
+ <div className = 'app'> | ||
+ {menu} | ||
+ {tabs} | ||
+ <Editor id={this.props.activeChallenge.id} /> | ||
+ </div> | ||
+ ); | ||
+ } else { | ||
+ | ||
+ return ( | ||
+ <div className = 'app'> | ||
+ {menu} | ||
+ {tabs} | ||
+ {selectChallenges} | ||
+ </div> | ||
+ ); | ||
+ } | ||
+ } | ||
+} | ||
+ | ||
+export default connector(GrandCentralStation); | ||
+ | ||
+GrandCentralStation.propTypes = { | ||
+ dispatch: React.PropTypes.func.isRequired, | ||
+ fileStore: React.PropTypes.object, | ||
+ activeFile: React.PropTypes.string, | ||
+ view: React.PropTypes.string.isRequired, | ||
+ activeChallenge: React.PropTypes.object | ||
+}; | ||
+ |
74
src/Components/Menu.js
@@ -0,0 +1,74 @@ | ||
+import React, { Component } from 'react'; | ||
+import {connect} from 'react-redux'; | ||
+ | ||
+import RaisedButton from 'material-ui/lib/raised-button'; | ||
+ | ||
+const styles = { | ||
+ fileInput: { | ||
+ cursor: 'pointer', | ||
+ position: 'absolute', | ||
+ top: '0', | ||
+ bottom: '0', | ||
+ right: '0', | ||
+ left: '0', | ||
+ width: '100%', | ||
+ opacity: '0' | ||
+ }, | ||
+ buttonStyle: { | ||
+ paddingRight: '5px' | ||
+ } | ||
+}; | ||
+ | ||
+const connector = connect(function(state) { | ||
+ return ( | ||
+ state | ||
+ ); | ||
+}, null, null, {pure: false}); | ||
+ | ||
+class Menu extends Component { | ||
+ | ||
+ constructor(props) { | ||
+ super(props); | ||
+ } | ||
+ | ||
+ render() { | ||
+ let MenuElements = this.props.elements.map((elem, ix) => { | ||
+ let potentialInput; | ||
+ if (elem.name === 'Choose File') { | ||
+ potentialInput = ( | ||
+ <input | ||
+ style = {styles.fileInput} | ||
+ type = 'file' multiple> | ||
+ </input> | ||
+ ); | ||
+ } | ||
+ return ( | ||
+ <span key = { ix } | ||
+ style = {styles.buttonStyle}> | ||
+ <RaisedButton key = {elem.name} | ||
+ label = { elem.name } | ||
+ onChange = {elem.handleChange} | ||
+ onClick = {elem.action} | ||
+ > | ||
+ {potentialInput} | ||
+ </RaisedButton> | ||
+ </span> | ||
+ ); | ||
+ }); | ||
+ | ||
+ return ( | ||
+ <div> | ||
+ <ul> | ||
+ {MenuElements} | ||
+ </ul> | ||
+ </div> | ||
+ ); | ||
+ } | ||
+} | ||
+ | ||
+export default connector(Menu); | ||
+ | ||
+Menu.propTypes = { | ||
+ elements: React.PropTypes.array.isRequired | ||
+}; | ||
+ |
44
src/Components/SelectChallenge.js
@@ -0,0 +1,44 @@ | ||
+import React, {Component} from 'react'; | ||
+import List from 'material-ui/lib/lists/list'; | ||
+import ListItem from 'material-ui/lib/lists/list-item'; | ||
+ | ||
+export default class SelectChallenge extends Component { | ||
+ | ||
+ constructor(props) { | ||
+ super(props); | ||
+ } | ||
+ | ||
+ handleClick(id) { | ||
+ this.props.challengeClick(id); | ||
+ } | ||
+ | ||
+ render() { | ||
+ let data = this.props.data.challenges.map((challenge) => { | ||
+ return ( | ||
+ <ListItem | ||
+ data-challengid = {challenge.id} | ||
+ key = {challenge.id} | ||
+ primaryText = {challenge.title} | ||
+ onClick = {this.handleClick.bind(this, challenge.id)} | ||
+ /> | ||
+ ); | ||
+ }); | ||
+ | ||
+ return ( | ||
+ <List> | ||
+ {data} | ||
+ <ListItem | ||
+ data-challengid = 'new' | ||
+ key = 'new' | ||
+ primaryText = 'Create new' | ||
+ onClick = {this.handleClick.bind(this, 'new')} | ||
+ /> | ||
+ </List> | ||
+ ); | ||
+ } | ||
+} | ||
+ | ||
+SelectChallenge.propTypes = { | ||
+ challengeClick: React.PropTypes.func, | ||
+ data: React.PropTypes.object | ||
+}; |
49
src/Components/Tabs.js
@@ -0,0 +1,49 @@ | ||
+import React, {Component} from 'react'; | ||
+import {Tab, Tabs} from 'material-ui/lib/tabs'; | ||
+ | ||
+export default class TabBar extends Component { | ||
+ | ||
+ constructor(props) { | ||
+ super(props); | ||
+ this.state = {'activeTab': '', 'tabChange': function() { | ||
+ this.props.action((arguments[1].split(/\=1\$/gi)[1]) | ||
+ .replace(/\=01/gi, '.')); | ||
+ this.setState({'activeTab': (arguments[1] | ||
+ .split(/\=1\$/gi)[1]).replace(/\=01/gi, '.')}); | ||
+ }}; | ||
+ } | ||
+ | ||
+ render() { | ||
+ let fileNames = []; | ||
+ let FileElements = []; | ||
+ for (let i in this.props.files) { | ||
+ let file = this.props.files[i]; | ||
+ if (file !== undefined) { | ||
+ fileNames.push(i); | ||
+ FileElements.push( | ||
+ <Tab key= { i } | ||
+ label = { i } | ||
+ onClick = {this.state.tabChange.bind(this)} | ||
+ value = { i } | ||
+ > | ||
+ {file.name} | ||
+ </Tab> | ||
+ ); | ||
+ } | ||
+ } | ||
+ return ( | ||
+ <Tabs onActive = { this.props.action } | ||
+ valueLink={{value: this.state.activeTab, | ||
+ requestChange: function() {console.log('tab changes');}} | ||
+ }> | ||
+ {FileElements} | ||
+ </Tabs> | ||
+ ); | ||
+ } | ||
+} | ||
+ | ||
+TabBar.propTypes = { | ||
+ action: React.PropTypes.func.isRequired, | ||
+ files: React.PropTypes.object.isRequired | ||
+}; | ||
+ |
48
src/actions/editorActions.js
@@ -0,0 +1,48 @@ | ||
+export function backAction(dispatch, payload) { | ||
+ dispatch({ | ||
+ type: 'backAction', | ||
+ payload | ||
+ }); | ||
+} | ||
+ | ||
+export function loadFile(dispatch, payload) { | ||
+ dispatch({ | ||
+ type: 'loadFile', | ||
+ payload | ||
+ }); | ||
+} | ||
+ | ||
+export function createChallenge(dispatch, payload) { | ||
+ dispatch({ | ||
+ type: 'createChallenge', | ||
+ payload | ||
+ }); | ||
+} | ||
+ | ||
+export function loadChallenge(dispatch, payload) { | ||
+ dispatch({ | ||
+ type: 'loadChallenge', | ||
+ payload | ||
+ }); | ||
+} | ||
+ | ||
+export function fileSelect(dispatch, payload) { | ||
+ dispatch({ | ||
+ type: 'fileSelect', | ||
+ payload | ||
+ }); | ||
+} | ||
+ | ||
+export function updateChallenge(dispatch, payload) { | ||
+ dispatch({ | ||
+ type: 'updateChallenge', | ||
+ payload | ||
+ }); | ||
+} | ||
+ | ||
+export function exportChallenge(payload) { | ||
+ return { | ||
+ type: 'exportChallenge', | ||
+ payload | ||
+ }; | ||
+} |
11
src/index.js
@@ -0,0 +1,11 @@ | ||
+import React from 'react'; | ||
+import ReactDOM from 'react-dom'; | ||
+import App from './App'; | ||
+import {Provider} from 'react-redux'; | ||
+import store from './store/store'; | ||
+ | ||
+ReactDOM.render( | ||
+ <Provider store = {store} ><App /></Provider>, | ||
+ document.getElementById('root') | ||
+ | ||
+); |
73
src/reducers/reducer.js
@@ -0,0 +1,73 @@ | ||
+const initialState = { | ||
+ 'fileStore': {}, | ||
+ 'activeFile': '', | ||
+ 'activeChallenge': {}, | ||
+ 'view': 'ChallengeSelect' | ||
+}; | ||
+ | ||
+function parser(key) { | ||
+ switch (key) { | ||
+ case 'description': | ||
+ case 'descriptionEs': | ||
+ case 'descriptionRu': | ||
+ case 'descriptionCn': | ||
+ case 'descriptionFr': | ||
+ case 'head': | ||
+ case 'tail': | ||
+ case 'challengeSeed': | ||
+ case 'MDNlinks': | ||
+ case 'solutions': // NOTE: This only works for one solution | ||
+ return function(val) { return val.split('\n'); }; | ||
+ case 'tests': | ||
+ return function(val) { return val.split('EOL\n'); }; | ||
+ default: | ||
+ return function(val) { return val; }; | ||
+ } | ||
+} | ||
+ | ||
+export default function(prevState = initialState, action) { | ||
+ switch (action.type) { | ||
+ | ||
+ case 'updateChallenge': | ||
+ let challenges = prevState.challenges.slice(); | ||
+ challenges = challenges.map(function(challenge) { | ||
+ if (challenge.id === action.payload.id) { | ||
+ Object.keys(action.payload.props).forEach(function(key) { | ||
+ action.payload.props[key] = parser(key)(action.payload.props[key]); | ||
+ }); | ||
+ return Object.assign({}, challenge, action.payload.props); | ||
+ } | ||
+ return (challenge); | ||
+ }); | ||
+ | ||
+ let fileStore = prevState.fileStore; | ||
+ let newFileStore = fileStore[prevState.activeFile]; | ||
+ | ||
+ newFileStore.challenges = challenges; | ||
+ | ||
+ fileStore[prevState.activeFile] = newFileStore; | ||
+ let newState = prevState; | ||
+ newState.challenges = challenges; | ||
+ Object.assign({}, newState, newFileStore); | ||
+ | ||
+ return (Object.assign({}, prevState, newState)); | ||
+ | ||
+ case 'createChallenge': | ||
+ return (Object.assign({}, prevState, action.payload)); | ||
+ | ||
+ case 'loadChallenge': | ||
+ return (Object.assign({}, prevState, action.payload)); | ||
+ | ||
+ case 'loadFile': | ||
+ return (Object.assign({}, prevState, action.payload)); | ||
+ | ||
+ case 'fileSelect': | ||
+ return (Object.assign({}, prevState, action.payload)); | ||
+ | ||
+ case 'backAction': | ||
+ return (Object.assign({}, prevState, action.payload)); | ||
+ | ||
+ default: | ||
+ return (prevState); | ||
+ } | ||
+} |
6
src/store/store.js
@@ -0,0 +1,6 @@ | ||
+import reducer from './../reducers/reducer'; | ||
+import {createStore} from 'redux'; | ||
+ | ||
+let store = createStore(reducer); | ||
+ | ||
+export default store; |
0
src/style.css
No changes.
3
views/error.jade
@@ -0,0 +1,3 @@ | ||
+h1= message | ||
+h2= error.status | ||
+pre #{error.stack} |
1
views/index.jade
@@ -0,0 +1 @@ | ||
+extends layout |
7
views/layout.jade
@@ -0,0 +1,7 @@ | ||
+doctype html | ||
+html | ||
+ head | ||
+ title= title | ||
+ body | ||
+ #root | ||
+ script(src = "/dist/bundle.js") |
25
webpack.config.js
@@ -0,0 +1,25 @@ | ||
+var path = require('path'); | ||
+ | ||
+module.exports = { | ||
+ devtool: 'eval', | ||
+ entry: [ | ||
+ './src/index.js' | ||
+ ], | ||
+ output: { | ||
+ path: path.join(__dirname, 'public/dist'), | ||
+ filename: 'bundle.js' | ||
+ }, | ||
+ module: { | ||
+ loaders: [ | ||
+ { | ||
+ test: /\.js$/, | ||
+ loaders: ['babel-loader'], | ||
+ include: path.join(__dirname, 'src') | ||
+ }, | ||
+ { | ||
+ test: /\.css$/, | ||
+ loaders: ['style', 'css'] | ||
+ } | ||
+ ] | ||
+ } | ||
+}; |
0 comments on commit
57330c2