├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── package.json ├── public └── index.html ├── screenshot-edit.png ├── screenshot-use.png ├── server.js ├── src ├── actions.js ├── components │ ├── Lesson.css │ ├── Lesson.js │ ├── LessonCode.css │ ├── LessonCode.js │ ├── LessonHeader.css │ ├── LessonHeader.js │ ├── LessonMenu.css │ ├── LessonMenu.js │ ├── LessonOutput.css │ ├── LessonOutput.js │ ├── LessonText.css │ ├── LessonText.js │ ├── Lessons.css │ ├── Lessons.js │ ├── LessonsToolbar.css │ └── LessonsToolbar.js ├── containers │ ├── LessonsApp.css │ └── LessonsApp.js ├── index.js ├── instructions-lesson.json ├── reducer.js ├── types.js └── utils │ ├── codemirror-jsx.js │ ├── export-json.js │ ├── parse-json-file.js │ └── uuid.js ├── webpack.build.config.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard", "standard-react"], 3 | "rules": { 4 | "arrow-spacing": 0, 5 | "brace-style": [2, "stroustrup", {"allowSingleLine": true}], 6 | "eqeqeq": [2, "smart"], 7 | "react/jsx-quotes": [2, "double", "avoid-escape"], 8 | "react/prop-types": 0, 9 | "react/wrap-multilines": 0, 10 | "space-before-function-paren": [2, "never"] 11 | }, 12 | "parser": "babel-eslint" 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/*.css 3 | public/*.js 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Jonny Buchanan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Lessons 2 | 3 | React Lessons is a tool for creating - and taking - interactive [React](http://facebook.github.io/react/) tutorials, inspired by the [Ractive.js Tutorial](http://learn.ractivejs.org). 4 | 5 | ## Taking tutorials 6 | 7 | A tutorial consists of a number of lessons. Each lesson can include one or more steps (numbered across the top-right of the page). 8 | 9 | ![Tutorial screenshot](screenshot-use.png) 10 | 11 | A lesson step consists of: 12 | 13 | * Prose providing learning material. 14 | 15 | * An outline for code to be written to practice the step's material. 16 | 17 | ### Writing JavaScript 18 | 19 | JavaScript can be written in the panel on the right and executed by pressing 20 | Shift+Enter or using the Execute button. 21 | 22 | The following variables are available for use in code: 23 | 24 | * `React` - the React library. 25 | * `output` - the DOM node for the output area below. 26 | 27 | Code is transformed with [Babel](http://babeljs.io) before being executed, so you can use: 28 | 29 | * [JSX](http://facebook.github.io/react/docs/jsx-in-depth.html) - the XML-like syntax React uses to make generating content in JavaScript more pleasant. 30 | * [ECMAScript 6 features](http://babeljs.io/docs/learn-es2015/#ecmascript-6-features) 31 | * [ECMAScript 7 proposals](http://babeljs.io/docs/usage/experimental/) experimentally supported by Babel. 32 | 33 | ## Editing tutorials 34 | 35 | Use the "Edit Mode" checkbox to toggle editing mode. 36 | 37 | ![Editing mode screenshot](screenshot-edit.png) 38 | 39 | In editing mode, you can change the lesson name and edit the content of each step. 40 | 41 | ### Step prose 42 | 43 | Step prose is written in [Markdown](http://daringfireball.net/projects/markdown/basics), with support for additional [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/) features. 44 | 45 | ### Step code & solution 46 | 47 | In editing mode, "Code" and "Solution" tabs will appear in the coding area: 48 | 49 | * Code is what the user will see in the coding panel when they open the step. 50 | 51 | **Note:** Code will be automatically executed each time the step is opened. 52 | 53 | * Solution (if provided) will allow use of the "Fix code" button to see a solution for the coding challenge. 54 | 55 | ### Creating and deleting lessons and steps 56 | 57 | In editing mode, extra toolbar buttons are also displayed to allow you to add new lessons and steps, or to delete the current lesson or step. 58 | 59 | When you add more lessons to a tutorial, a menu will pop up on the left side of the page to allow you to navigate between them. 60 | 61 | ### Exporting 62 | 63 | You can export the current lesson using the "Export Lesson" button, or the complete tutorial using the "Export Tutorial" button. 64 | 65 | You will be prompted to download a `.json` file containing exported data. 66 | 67 | ### Importing 68 | 69 | To import a lesson or a tutorial, use the "Import Lesson(s)" button to select a `.json` file, or drag and drop a `.json` file anywhere on the page. 70 | 71 | **Warning:** if you import a tutorial, its lessons will replace *everything* you currently have. 72 | 73 | ## MIT Licensed 74 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Lessons 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lessons", 3 | "version": "1.0.0", 4 | "description": "A tool for creating - and taking - interactive React tutorials", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/insin/react-lessons.git" 8 | }, 9 | "keywords": [ 10 | "react", 11 | "lessons", 12 | "tutorials" 13 | ], 14 | "scripts": { 15 | "build": "webpack --config=webpack.build.config.js", 16 | "lint": "eslint .", 17 | "start": "node server.js" 18 | }, 19 | "devDependencies": { 20 | "autoprefixer-loader": "^2.0.0", 21 | "babel-core": "^5.7.4", 22 | "babel-eslint": "^3.1.23", 23 | "babel-loader": "^5.3.2", 24 | "classnames": "^2.1.3", 25 | "css-loader": "^0.15.4", 26 | "eslint": "^1.0.0-rc-1", 27 | "eslint-config-standard": "^4.0.0-1", 28 | "eslint-config-standard-react": "^1.0.2", 29 | "eslint-plugin-react": "^2.7.1", 30 | "extract-text-webpack-plugin": "^0.8.2", 31 | "json-loader": "^0.5.2", 32 | "less": "^2.5.1", 33 | "less-loader": "^2.2.0", 34 | "react-hot-loader": "^1.2.8", 35 | "style-loader": "^0.12.3", 36 | "webpack": "^1.10.1", 37 | "webpack-dev-server": "^1.10.1" 38 | }, 39 | "dependencies": { 40 | "base-64": "^0.1.0", 41 | "boxxy": "^0.2.2", 42 | "classnames": "^2.1.2", 43 | "codemirror": "^5.4.0", 44 | "github-markdown-css": "^2.0.9", 45 | "marked": "^0.3.3", 46 | "react": "^0.13.3", 47 | "react-codemirror": "^0.1.2", 48 | "react-redux": "^0.2.2", 49 | "react-router": "^1.0.0-beta3", 50 | "redux": "^1.0.0-rc", 51 | "redux-action-utils": "^1.0.0", 52 | "redux-thunk": "^0.1.0" 53 | }, 54 | "license": "MIT" 55 | } 56 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Lessons 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /screenshot-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/react-lessons/b3c91251f00fe0fba247216a69bac97c3088fe41/screenshot-edit.png -------------------------------------------------------------------------------- /screenshot-use.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/react-lessons/b3c91251f00fe0fba247216a69bac97c3088fe41/screenshot-use.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var WebpackDevServer = require('webpack-dev-server') 3 | var config = require('./webpack.config') 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true 9 | }).listen(3000, 'localhost', function(err, result) { 10 | if (err) { 11 | console.log(err) 12 | } 13 | console.log('Listening at localhost:3000') 14 | }) 15 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | var {action} = require('redux-action-utils') 2 | var types = require('./types') 3 | 4 | module.exports = { 5 | addLesson: action(types.ADD_LESSON), 6 | addStep: action(types.ADD_STEP), 7 | deleteLesson: action(types.DELETE_LESSON), 8 | deleteStep: action(types.DELETE_STEP), 9 | executeCode: action(types.EXECUTE_CODE, 'code'), 10 | importLessons: action(types.IMPORT_LESSONS, 'imported'), 11 | selectLesson: action(types.SELECT_LESSON, 'lessonIndex'), 12 | selectStep: action(types.SELECT_STEP, 'stepIndex'), 13 | toggleEditing: action(types.TOGGLE_EDITING, 'editing'), 14 | updateCode: action(types.UPDATE_CODE, 'code'), 15 | updateLesson: action(types.UPDATE_LESSON, 'update'), 16 | updateStep: action(types.UPDATE_STEP, 'update') 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Lesson.css: -------------------------------------------------------------------------------- 1 | .Lesson { 2 | flex-grow: 1; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: stretch; 6 | } 7 | .Lesson__boxxy { 8 | flex-grow: 1; 9 | position: relative; 10 | } 11 | .boxxy-inner { 12 | padding: 0.3em; 13 | box-shadow: inset 1px 1px 4px rgba(0,0,0,0.3); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Lesson.js: -------------------------------------------------------------------------------- 1 | var Boxxy = require('boxxy') 2 | var React = require('react') 3 | 4 | var LessonCode = require('./LessonCode') 5 | var LessonHeader = require('./LessonHeader') 6 | var LessonOutput = require('./LessonOutput') 7 | var LessonText = require('./LessonText') 8 | 9 | require('./Lesson.css') 10 | 11 | var BOXXY_CONFIG = { 12 | columns: [ 13 | { 14 | id: 'left', 15 | size: 50, 16 | children: [ 17 | {id: 'lesson', size: 65}, 18 | {id: 'output', size: 35} 19 | ] 20 | }, 21 | { 22 | id: 'code', 23 | size: 50 24 | } 25 | ] 26 | } 27 | 28 | var Lesson = React.createClass({ 29 | // Lesson contents are being independently rendered into DOM elements managed 30 | // by Boxxy (a.k.a. portals). This breaks the context chain between these 31 | // portal-rendered components. As such, we must grab anything needed by these 32 | // components out of context and pass it manually. 33 | contextTypes: { 34 | router: React.PropTypes.object.isRequired 35 | }, 36 | componentDidMount() { 37 | this.boxxy = new Boxxy(React.findDOMNode(this.refs.boxxy), BOXXY_CONFIG) 38 | this.renderBoxxyContent() 39 | }, 40 | componentWillReceiveProps(nextProps) { 41 | if (this.props.currentLessonIndex !== nextProps.currentLessonIndex || 42 | this.props.currentStepIndex !== nextProps.currentStepIndex) { 43 | this.boxxy.blocks.lesson.scrollTop = 0 44 | } 45 | this.renderBoxxyContent(nextProps) 46 | }, 47 | componentWillUnmount() { 48 | var {lesson, output, code} = this.boxxy.blocks 49 | React.unmountComponentAtNode(lesson) 50 | React.unmountComponentAtNode(output) 51 | React.unmountComponentAtNode(code) 52 | }, 53 | render() { 54 | return
55 | 56 |
57 |
58 | }, 59 | renderBoxxyContent(props) { 60 | props = props || this.props 61 | var {lesson, output, code} = this.boxxy.blocks 62 | React.render(, lesson) 63 | React.render(, output) 64 | React.render(, code) 65 | } 66 | }) 67 | 68 | module.exports = Lesson 69 | -------------------------------------------------------------------------------- /src/components/LessonCode.css: -------------------------------------------------------------------------------- 1 | .LessonCode { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column 5 | } 6 | .LessonCode .ReactCodeMirror { 7 | flex: 1; 8 | overflow: hidden; 9 | } 10 | .LessonCode__tabs { 11 | position: relative; 12 | background-color: #f7f7f7; 13 | text-align: right; 14 | } 15 | .LessonCode__tabs:before { 16 | z-index: 1; 17 | } 18 | .LessonCode__tabs:after { 19 | position: absolute; 20 | content: ""; 21 | width: 100%; 22 | bottom: 0; 23 | left: 0; 24 | border-bottom: 1px solid #ddd; 25 | z-index: 1; 26 | } 27 | .LessonCode__tab { 28 | display: inline-block; 29 | padding: .3em 1em; 30 | cursor: pointer; 31 | border-left: 1px solid #ddd; 32 | border-top: 1px solid #ddd; 33 | border-right: 1px solid #ddd; 34 | color: #777; 35 | position: relative; 36 | z-index: 0; 37 | margin-right: 5px; 38 | border-top-left-radius: 6px; 39 | border-top-right-radius: 6px; 40 | } 41 | .LessonCode__tab:hover { 42 | color: #444; 43 | background-color: #fbfbfb; 44 | } 45 | .LessonCode__tab--active { 46 | cursor: default; 47 | color: #000 !important; 48 | background-color: #fff !important; 49 | border-bottom: 1px solid #fff; 50 | z-index: 2; 51 | } 52 | .LessonCode__buttons { 53 | text-align: right; 54 | } 55 | .LessonCode__execute { 56 | box-sizing: border-box; 57 | width: 100%; 58 | padding: .3em .5em; 59 | } 60 | -------------------------------------------------------------------------------- /src/components/LessonCode.js: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames') 2 | var CodeMirror = require('react-codemirror') 3 | var React = require('react') 4 | 5 | require('./LessonCode.css') 6 | 7 | function tabClassNames(tab, activeTab) { 8 | return classNames('LessonCode__tab', { 9 | 'LessonCode__tab--active': tab === activeTab 10 | }) 11 | } 12 | 13 | var LessonCode = React.createClass({ 14 | getInitialState() { 15 | return { 16 | activeTab: 'code' 17 | } 18 | }, 19 | componentWillReceiveProps(nextProps) { 20 | if (this.props.editing !== nextProps.editing || 21 | this.props.currentLessonIndex !== nextProps.currentLessonIndex || 22 | this.props.currentStepIndex !== nextProps.currentStepIndex) { 23 | this.setState({ 24 | activeTab: 'code' 25 | }) 26 | } 27 | }, 28 | handleChangeTab(activeTab) { 29 | this.setState({activeTab}) 30 | }, 31 | handleExecuteCode() { 32 | this.props.executeCode(this.props.editing 33 | ? this.props.step[this.state.activeTab] 34 | : this.props.code) 35 | }, 36 | handleChange(code) { 37 | if (!this.props.editing) { 38 | if (code !== this.props.code) { 39 | this.props.updateCode(code) 40 | } 41 | } 42 | else { 43 | if (code !== this.props.step[this.state.activeTab]) { 44 | this.props.updateStep({[this.state.activeTab]: code}) 45 | } 46 | } 47 | }, 48 | render() { 49 | var {editing, step} = this.props 50 | var {activeTab} = this.state 51 | return
52 | {editing &&
53 |
55 | Code 56 |
57 |
59 | Solution 60 |
61 |
} 62 | 72 |
73 | 76 |
77 |
78 | } 79 | }) 80 | 81 | module.exports = LessonCode 82 | -------------------------------------------------------------------------------- /src/components/LessonHeader.css: -------------------------------------------------------------------------------- 1 | .LessonHeader { 2 | padding-bottom: .5em; 3 | display: flex; 4 | } 5 | .LessonHeader h2 { 6 | margin: 0; 7 | flex: 1; 8 | display: flex; 9 | } 10 | .LessonHeader__number { 11 | margin-right: .3em; 12 | } 13 | .LessonHeader__name { 14 | flex: 1; 15 | } 16 | .LessonHeader input { 17 | font: inherit; 18 | box-sizing: border-box; 19 | width: 100%; 20 | padding-left: .3em; 21 | border: none; 22 | box-shadow: inset 1px 1px 4px rgba(0,0,0,0.3); 23 | } 24 | .LessonHeader__steps { 25 | display: flex; 26 | align-items: stretch; 27 | margin-left: .3em; 28 | } 29 | .LessonHeader__step { 30 | width: 2em; 31 | line-height: 1.9em; 32 | text-align: center; 33 | cursor: pointer; 34 | border-radius: 50%; 35 | background-color: #ddd; 36 | margin-right: .3em; 37 | color: #444; 38 | text-decoration: none; 39 | } 40 | .LessonHeader__step:last-child { 41 | margin-right: 0; 42 | } 43 | .LessonHeader__step--active { 44 | background-color: #333; 45 | color: #fff; 46 | cursor: default; 47 | } 48 | .LessonHeader__buttons { 49 | display: flex; 50 | align-items: stretch; 51 | margin-left: .3em; 52 | } 53 | .LessonHeader__buttons button { 54 | margin-right: .3em; 55 | } 56 | .LessonHeader__buttons button:last-child { 57 | margin-right: 0; 58 | } 59 | -------------------------------------------------------------------------------- /src/components/LessonHeader.js: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames') 2 | var React = require('react') 3 | var {Link} = require('react-router') 4 | 5 | require('./LessonHeader.css') 6 | 7 | var LessonHeader = React.createClass({ 8 | handleChange(e) { 9 | this.props.updateLesson({name: e.target.value}) 10 | }, 11 | handleFixCode() { 12 | this.props.updateCode(this.props.step.solution) 13 | this.props.executeCode(this.props.step.solution) 14 | }, 15 | handleReset() { 16 | this.props.updateCode(this.props.step.code) 17 | this.props.executeCode(this.props.step.code) 18 | }, 19 | render() { 20 | var {currentLessonIndex, currentStepIndex, editing, lesson, lessonCount, lessonNumber, step} = this.props 21 | return
22 |

23 | {lessonCount > 1 && {lessonNumber}.} 24 | 25 | {editing 26 | ? 27 | : lesson.name 28 | } 29 | 30 |

31 |
32 | {lesson.steps.map((step, index) => 33 | 37 | {index + 1} 38 | 39 | )} 40 |
41 | {!editing &&
42 | 43 | 44 |
} 45 |
46 | } 47 | }) 48 | 49 | module.exports = LessonHeader 50 | -------------------------------------------------------------------------------- /src/components/LessonMenu.css: -------------------------------------------------------------------------------- 1 | .LessonMenu { 2 | padding-right: .5em; 3 | } 4 | .LessonMenu__lesson { 5 | color: #444; 6 | text-decoration: none; 7 | } 8 | .LessonMenu__number { 9 | display: block; 10 | width: 1.6em; 11 | height: 1.6em; 12 | line-height: 1.7em; 13 | text-align: center; 14 | cursor: pointer; 15 | margin-bottom: .5em; 16 | } 17 | .LessonMenu__lesson--active .LessonMenu__number { 18 | background-color: #333; 19 | color: #fff; 20 | border-radius: 50%; 21 | cursor: default; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/LessonMenu.js: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames') 2 | var React = require('react') 3 | var {Link} = require('react-router') 4 | 5 | require('./LessonMenu.css') 6 | 7 | var LessonMenu = React.createClass({ 8 | render() { 9 | var {lessons, currentLessonIndex} = this.props 10 | return
11 | {lessons.map((lesson, index) => 12 | 16 |
17 | {index + 1} 18 |
19 | 20 | )} 21 |
22 | } 23 | }) 24 | 25 | module.exports = LessonMenu 26 | -------------------------------------------------------------------------------- /src/components/LessonOutput.css: -------------------------------------------------------------------------------- 1 | .LessonOutput { 2 | padding: .5em; 3 | } 4 | .LessonOutput p { 5 | margin: 0 0 .5em; 6 | } 7 | .LessonOutput__error { 8 | background-color: #fdd; 9 | color: #f00; 10 | position: absolute; 11 | bottom: 0; 12 | left: 0; 13 | right: 0; 14 | padding: .5em; 15 | border: 1px solid #f99; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/LessonOutput.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var babel = require('babel-core/browser') 3 | 4 | require('./LessonOutput.css') 5 | 6 | var BABEL_OPTIONS = {stage: 0} 7 | 8 | var LessonOutput = React.createClass({ 9 | getInitialState() { 10 | return { 11 | errorMessage: '' 12 | } 13 | }, 14 | componentDidMount() { 15 | if (this.props.executedCode) { 16 | this.executeCode(this.props.executedCode) 17 | } 18 | }, 19 | componentWillReceiveProps(nextProps) { 20 | if (this.props.executedCode !== nextProps.executedCode) { 21 | this.executeCode(nextProps.executedCode) 22 | } 23 | }, 24 | executeCode(code) { 25 | var output = React.findDOMNode(this.refs.output) 26 | if (!React.unmountComponentAtNode(output)) { 27 | output.innerHTML = '' 28 | } 29 | var errorMessage = '' 30 | if (code) { 31 | try { 32 | /* eslint-disable no-new-func */ 33 | var func = new Function('React', 'output', babel.transform(code, BABEL_OPTIONS).code) 34 | /* eslint-enable no-new-func */ 35 | func.call(this, React, output) 36 | } 37 | catch (e) { 38 | errorMessage = e.message 39 | React.unmountComponentAtNode(output) 40 | } 41 | } 42 | this.setState({errorMessage}) 43 | }, 44 | render() { 45 | return
46 | {this.state.errorMessage &&
47 | {this.state.errorMessage} 48 |
} 49 |
50 |
51 | } 52 | }) 53 | 54 | module.exports = LessonOutput 55 | -------------------------------------------------------------------------------- /src/components/LessonText.css: -------------------------------------------------------------------------------- 1 | .LessonText { 2 | height: 100%; 3 | } 4 | .LessonText__next { 5 | padding: 1em; 6 | text-align: right; 7 | font-size: 1.5em; 8 | } 9 | .LessonText__next a { 10 | color: #4078C0; 11 | text-decoration: none; 12 | } 13 | .LessonText__next a:hover { 14 | outline: 0px none; 15 | text-decoration: underline; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/LessonText.js: -------------------------------------------------------------------------------- 1 | var CodeMirror = require('react-codemirror') 2 | var marked = require('marked') 3 | var React = require('react') 4 | var {Link} = require('react-router') 5 | 6 | require('./LessonText.css') 7 | 8 | var LessonText = React.createClass({ 9 | // Since this component is being rendered into a portal and needs to use React 10 | // Router's component, we need to manually recreate the context it 11 | // requires using the router instance which is being passed in as a prop. 12 | childContextTypes: { 13 | router: React.PropTypes.object.isRequired 14 | }, 15 | getChildContext() { 16 | return { 17 | router: this.props.router 18 | } 19 | }, 20 | shouldComponentUpdate(nextProps) { 21 | return (this.props.step.text !== nextProps.step.text || 22 | this.props.editing !== nextProps.editing || 23 | this.props.stepNumber !== nextProps.stepNumber || 24 | this.props.lessonNumber !== nextProps.lessonNumber) 25 | }, 26 | handleChange(text) { 27 | if (text !== this.props.step.text) { 28 | this.props.updateStep({text}) 29 | } 30 | }, 31 | render() { 32 | var {currentLessonIndex, currentStepIndex, editing, step, stepNumber, lesson, lessonNumber, lessons} = this.props 33 | var hasNextStep = stepNumber < lesson.steps.length 34 | var hasNextLesson = stepNumber === lesson.steps.length && lessonNumber < lessons.length 35 | return
36 | {editing 37 | ? 42 | :
43 |
44 | {hasNextStep &&
45 | 46 | next » 47 | 48 |
} 49 | {hasNextLesson && !!step.text &&
50 | 51 | next lesson » 52 | 53 |
} 54 |
55 | } 56 |
57 | } 58 | }) 59 | 60 | module.exports = LessonText 61 | -------------------------------------------------------------------------------- /src/components/Lessons.css: -------------------------------------------------------------------------------- 1 | .Lessons { 2 | height: 100%; 3 | display: flex; 4 | align-items: stretch; 5 | } -------------------------------------------------------------------------------- /src/components/Lessons.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var Lesson = require('./Lesson') 3 | var LessonMenu = require('./LessonMenu') 4 | 5 | require('./Lessons.css') 6 | 7 | var Lessons = React.createClass({ 8 | render() { 9 | var {lessons, currentLessonIndex, currentStepIndex, actions} = this.props 10 | var currentLesson = lessons[currentLessonIndex] 11 | var currentStep = currentLesson.steps[currentStepIndex] 12 | return
13 | {lessons.length > 1 &&
14 | 19 |
} 20 | 29 |
30 | } 31 | }) 32 | 33 | module.exports = Lessons 34 | -------------------------------------------------------------------------------- /src/components/LessonsToolbar.css: -------------------------------------------------------------------------------- 1 | .LessonsToolbar { 2 | border: 2px dotted #000; 3 | padding: .5em; 4 | margin-bottom: .5em; 5 | } 6 | .LessonsToolbar__file { 7 | position: relative; 8 | overflow: hidden; 9 | display: inline-block; 10 | vertical-align: bottom; 11 | } 12 | .LessonsToolbar__file > button { 13 | z-index: 1; 14 | } 15 | .LessonsToolbar__file > input[type=file] { 16 | opacity: 0; 17 | z-index: 2; 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | right: 0; 22 | bottom: 0; 23 | } 24 | .LessonsToolbar a { 25 | color: #333; 26 | text-decoration: none; 27 | font-weight: bold; 28 | } 29 | .LessonsToolbar a:hover { 30 | color: #4078C0; 31 | outline: 0px none; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/LessonsToolbar.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | 3 | var instructionsLesson = require('../instructions-lesson') 4 | var exportJSON = require('../utils/export-json') 5 | var parseJSONFile = require('../utils/parse-json-file') 6 | 7 | require('./LessonsToolbar.css') 8 | 9 | function findInstructionsIndex(lessons) { 10 | var instructionsIndex = -1 11 | for (var i = 0; i < lessons.length; i++) { 12 | if (lessons[i].instructions) { 13 | instructionsIndex = i 14 | break 15 | } 16 | } 17 | return instructionsIndex 18 | } 19 | 20 | var LessonsToolbar = React.createClass({ 21 | contextTypes: { 22 | router: React.PropTypes.object.isRequired 23 | }, 24 | 25 | handleExportLesson(lesson) { 26 | exportJSON(lesson, lesson.name ? `${lesson.name}.lesson.json` : 'lesson.json') 27 | }, 28 | 29 | handleExportTutorial(lessons) { 30 | exportJSON(lessons, 'react-lessons.tutorial.json') 31 | }, 32 | 33 | handleViewInstructions() { 34 | var {actions, lessons, currentLessonIndex} = this.props 35 | var instructionsIndex = findInstructionsIndex(lessons) 36 | 37 | // Only add the instructions lesson if it's not already there 38 | if (instructionsIndex === -1) { 39 | actions.importLessons(instructionsLesson) 40 | instructionsIndex = lessons.length 41 | } 42 | 43 | // Only select the instructions lesson if we're not already doing so, as it 44 | // will switch back to the first step if already selected. 45 | if (instructionsIndex !== currentLessonIndex) { 46 | this.context.router.transitionTo(`/${instructionsIndex}/0`) 47 | } 48 | }, 49 | 50 | handleFileChange(e) { 51 | if (!e.target.files[0]) { 52 | return 53 | } 54 | parseJSONFile(e.target.files[0], (err, lessonData) => { 55 | if (err) { 56 | window.alert(`Unable to import lessons: ${err.message}.`) 57 | return 58 | } 59 | var {lessons} = this.props 60 | var {router} = this.context 61 | this.props.actions.importLessons(lessonData) 62 | if (Array.isArray(lessonData)) { 63 | router.replaceWith('/0/0') 64 | } 65 | else { 66 | router.transitionTo(`/${lessons.length}/0`) 67 | } 68 | }) 69 | }, 70 | 71 | handleAddLesson() { 72 | var {lessons} = this.props 73 | this.props.actions.addLesson() 74 | this.context.router.transitionTo(`/${lessons.length}/0`) 75 | }, 76 | 77 | handleDeleteLesson() { 78 | var {currentLessonIndex, currentStepIndex, lessons} = this.props 79 | this.props.actions.deleteLesson() 80 | if (currentLessonIndex === lessons.length - 1) { 81 | this.context.router.replaceWith(`/${currentLessonIndex - 1}/0`) 82 | } 83 | else if (currentStepIndex > 0) { 84 | this.context.router.replaceWith(`/${currentLessonIndex}/0`) 85 | } 86 | }, 87 | 88 | handleAddStep() { 89 | var {currentLesson, currentLessonIndex} = this.props 90 | this.props.actions.addStep() 91 | this.context.router.transitionTo(`/${currentLessonIndex}/${currentLesson.steps.length}`) 92 | }, 93 | 94 | handleDeleteStep() { 95 | var {currentStepIndex, currentLesson, currentLessonIndex} = this.props 96 | this.props.actions.deleteStep() 97 | if (currentStepIndex === currentLesson.steps.length - 1) { 98 | this.context.router.replaceWith(`/${currentLessonIndex}/${currentStepIndex - 1}`) 99 | } 100 | }, 101 | 102 | render() { 103 | var {actions, currentLesson, currentLessonIndex, editing, lessons} = this.props 104 | var instructionsIndex = findInstructionsIndex(lessons) 105 | var showViewInstructions = instructionsIndex === -1 || instructionsIndex !== currentLessonIndex 106 | return
107 | Fork me on GitHub 108 | 115 | {' | '} 116 | 117 | 118 | 119 | 120 | {' | '} 121 | 124 | {' | '} 125 | 128 | {editing && {' | '} 129 | {' | '} 130 | {lessons.length > 1 && } 133 | {' | '} 134 | {' '} 135 | {currentLesson.steps.length > 1 && {' | '} 136 | 139 | } 140 | } 141 | {showViewInstructions && {' | '} 142 | 145 | } 146 |
147 | } 148 | }) 149 | 150 | module.exports = LessonsToolbar 151 | -------------------------------------------------------------------------------- /src/containers/LessonsApp.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | body { 5 | font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif; 6 | margin: 0; 7 | height: 100%; 8 | } 9 | #app { 10 | height: 100%; 11 | } 12 | .LessonsApp { 13 | display: flex; 14 | align-items: stretch; 15 | flex-direction: column; 16 | height: 100%; 17 | padding: 8px; 18 | box-sizing: border-box; 19 | } 20 | .markdown-body { 21 | padding: .5em; 22 | } 23 | .markdown-body > :first-child { 24 | margin-top: 0 !important; 25 | } 26 | .ReactCodeMirror, .CodeMirror { 27 | height: 100%; 28 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 29 | font-size: 14px; 30 | } 31 | -------------------------------------------------------------------------------- /src/containers/LessonsApp.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var {bindActionCreators} = require('redux') 3 | var {connect} = require('react-redux') 4 | 5 | var LessonActions = require('../actions') 6 | var Lessons = require('../components/Lessons') 7 | var LessonsToolbar = require('../components/LessonsToolbar') 8 | var parseJSONFile = require('../utils/parse-json-file') 9 | 10 | require('./LessonsApp.css') 11 | 12 | var LessonsApp = connect(state => state)(React.createClass({ 13 | contextTypes: { 14 | router: React.PropTypes.object.isRequired 15 | }, 16 | 17 | componentWillMount() { 18 | var {dispatch, params} = this.props 19 | dispatch(LessonActions.selectLesson(Number(params.lesson))) 20 | dispatch(LessonActions.selectStep(Number(params.step))) 21 | }, 22 | 23 | componentWillReceiveProps(nextProps) { 24 | var {dispatch, params} = this.props 25 | var {params: nextParams} = nextProps 26 | if (params.lesson !== nextParams.lesson) { 27 | dispatch(LessonActions.selectLesson(Number(nextParams.lesson))) 28 | } 29 | if (params.step !== nextParams.step) { 30 | dispatch(LessonActions.selectStep(Number(nextParams.step))) 31 | } 32 | }, 33 | 34 | handleDragOver(e) { 35 | e.preventDefault() 36 | e.dataTransfer.dropEffect = 'copy' 37 | }, 38 | 39 | handleDrop(e) { 40 | e.preventDefault() 41 | if (!e.dataTransfer.files || !e.dataTransfer.files[0]) { 42 | return 43 | } 44 | parseJSONFile(e.dataTransfer.files[0], (err, lessonData) => { 45 | if (err) { 46 | window.alert(`Unable to import lessons: ${err.message}.`) 47 | return 48 | } 49 | var {dispatch, lessons} = this.props 50 | var {router} = this.context 51 | dispatch(LessonActions.importLessons(lessonData)) 52 | if (Array.isArray(lessonData)) { 53 | router.replaceWith('/0/0') 54 | } 55 | else { 56 | router.transitionTo(`/${lessons.length}/0`) 57 | } 58 | }) 59 | }, 60 | 61 | render() { 62 | var {dispatch, ...props} = this.props 63 | var {currentLessonIndex, currentStepIndex, lessons} = props 64 | var actions = bindActionCreators(LessonActions, dispatch) 65 | var currentLesson = lessons[currentLessonIndex] 66 | var currentStep = currentLesson.steps[currentStepIndex] 67 | return
68 | 69 | 70 |
71 | } 72 | })) 73 | 74 | module.exports = LessonsApp 75 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require('github-markdown-css/github-markdown.css') 2 | require('codemirror/lib/codemirror.css') 3 | 4 | require('codemirror') 5 | require('codemirror/addon/mode/overlay') 6 | require('codemirror/addon/mode/multiplex') 7 | require('codemirror/mode/javascript/javascript') 8 | require('codemirror/mode/markdown/markdown') 9 | require('codemirror/mode/xml/xml') 10 | require('codemirror/mode/gfm/gfm') 11 | require('./utils/codemirror-jsx') 12 | 13 | var React = require('react') 14 | var {applyMiddleware, createStore} = require('redux') 15 | var {Provider} = require('react-redux') 16 | var thunkMiddleware = require('redux-thunk') 17 | var {Redirect, Router, Route} = require('react-router') 18 | var {history} = require('react-router/lib/HashHistory') 19 | 20 | var LessonsApp = require('./containers/LessonsApp') 21 | var reducer = require('./reducer') 22 | 23 | var renderRoutes = () => 24 | 25 | 26 | 27 | 28 | 29 | var store = applyMiddleware(thunkMiddleware)(createStore)(reducer) 30 | 31 | React.render( 32 | {renderRoutes}, 33 | document.getElementById('app') 34 | ) 35 | 36 | -------------------------------------------------------------------------------- /src/instructions-lesson.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Lessons Instructions", 3 | "instructions": true, 4 | "steps": [ 5 | { 6 | "text": "React Lessons is a tool for creating - and taking - interactive [React](http://facebook.github.io/react/) tutorials, inspired by the [Ractive.js Tutorial](http://learn.ractivejs.org).\n\nA tutorial consists of a number of lessons. Each lesson can include one or more steps (numbered across the top-right of the page).\n\nA lesson step consists of:\n\n* Prose providing learning material.\n\n* An outline for code to be written to practice the step's material.\n\nClick the \"next\" link below to proceed to the next step and try some coding.", 7 | "code": "", 8 | "solution": "" 9 | }, 10 | { 11 | "text": "## Writing JavaScript\n\nJavaScript can be written in the panel on the right and executed by pressing\nShift+Enter or using the Execute button.\n\nThe following variables are available for use in code:\n\n* `React` - the React library.\n* `output` - the DOM node for the output area below.\n\nCode is transformed with [Babel](http://babeljs.io) before being executed, so you can use:\n\n* [JSX](http://facebook.github.io/react/docs/jsx-in-depth.html) - the XML-like syntax React uses to make generating content in JavaScript more pleasant.\n* [ECMAScript 6 features](http://babeljs.io/docs/learn-es2015/#ecmascript-6-features)\n* [ECMAScript 7 proposals](http://babeljs.io/docs/usage/experimental/) experimentally supported by Babel.\n\n### Coding time!\n\nLet's do some coding practice to get started.\n\nModify the code on the right so the output reads \"Hello world!\" instead of \"Hello…\"\n\nIf you make a mess of it, click the \"Reset\" button to revert your changes.\n\nIf you're stumped, click the \"Fix code\" button to view and run the solution.", 12 | "code": "React.render(

Hello…

, output)", 13 | "solution": "React.render(

Hello world!

, output)" 14 | }, 15 | { 16 | "text": "## Editing lessons\n\nUse the \"Edit Mode\" checkbox to toggle editing mode.\n\nIn editing mode, you can change the lesson name and edit the content of each step.\n\n### Step prose\n\nStep prose is written in [Markdown](http://daringfireball.net/projects/markdown/basics), with support for additional [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/) features.\n\n### Step code & solution\n\nIn editing mode, \"Code\" and \"Solution\" tabs will appear in the coding area:\n\n* Code is what the user will see in the coding panel when they open the step.\n\n **Note:** Code will be automatically executed each time the step is opened.\n\n* Solution (if provided) will allow use of the \"Fix code\" button to see a solution for the coding challenge.", 17 | "code": "BadSyntax", 18 | "solution": "React.render(

Good syntax.

, output)" 19 | }, 20 | { 21 | "text": "## Creating and deleting lessons and steps\n\nIn editing mode, extra toolbar buttons are also displayed to allow you to add new lessons and steps, or to delete the current lesson or step.\n\nWhen you add more lessons to a tutorial, a menu will pop up on the left side of the page to allow you to navigate between them.\n\nClick \"Add Lesson\" now to give it a go.", 22 | "code": "", 23 | "solution": "" 24 | }, 25 | { 26 | "text": "## Exporting lessons and tutorials\n\nYou can export the current lesson using the \"Export Lesson\" button, or the complete tutorial using the \"Export Tutorial\" button.\n\nYou will be prompted to download a `.json` file containing exported data.\n\n## Importing lessons and tutorials\n\nTo import a lesson or a tutorial, use the \"Import Lesson(s)\" button to select a `.json` file, or drag and drop a `.json` file anywhere on the page.\n\n**Warning:** if you import a tutorial, its lessons will replace *everything* you currently have.", 27 | "code": "", 28 | "solution": "" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | var update = require('react/lib/update') 2 | 3 | var types = require('./types') 4 | var uuid = require('./utils/uuid') 5 | 6 | function createStep() { 7 | return {id: uuid(), text: '', code: '', solution: ''} 8 | } 9 | 10 | function createLesson() { 11 | return {id: uuid(), name: '', steps: [createStep()]} 12 | } 13 | 14 | var defaultState = { 15 | code: '', 16 | currentLessonIndex: 0, 17 | currentStepIndex: 0, 18 | editing: true, 19 | executedCode: '', 20 | lessons: [createLesson()] 21 | } 22 | 23 | module.exports = function lessons(state=defaultState, action) { 24 | var code 25 | 26 | switch (action.type) { 27 | case types.ADD_LESSON: 28 | return { 29 | ...state, 30 | lessons: update(state.lessons, {$push: [createLesson()]}), 31 | currentLessonIndex: state.lessons.length, 32 | currentStepIndex: 0 33 | } 34 | case types.ADD_STEP: 35 | return { 36 | ...state, 37 | lessons: update(state.lessons, { 38 | [state.currentLessonIndex]: { 39 | steps: {$push: [createStep()]} 40 | } 41 | }), 42 | currentStepIndex: state.lessons[state.currentLessonIndex].steps.length 43 | } 44 | // Assumption: you can only delete lessons when there is more than one 45 | case types.DELETE_LESSON: 46 | return { 47 | ...state, 48 | lessons: update(state.lessons, {$splice: [[state.currentLessonIndex, 1]]}), 49 | // If the last lesson is being deleted, we need to adjust the current index 50 | currentLessonIndex: Math.min(state.currentLessonIndex, state.lessons.length - 2), 51 | currentStepIndex: 0 52 | } 53 | // Assumption: you can only delete lesson steps when there is more than one 54 | case types.DELETE_STEP: 55 | return { 56 | ...state, 57 | lessons: update(state.lessons, { 58 | [state.currentLessonIndex]: { 59 | steps: {$splice: [[state.currentStepIndex, 1]]} 60 | } 61 | }), 62 | // If the last step is being deleted, we need to adjust the current index 63 | currentStepIndex: Math.min(state.currentStepIndex, 64 | state.lessons[state.currentLessonIndex].steps.length - 2) 65 | } 66 | case types.EXECUTE_CODE: 67 | return {...state, executedCode: action.code} 68 | case types.IMPORT_LESSONS: 69 | if (Array.isArray(action.imported)) { 70 | return { 71 | ...state, 72 | lessons: action.imported, 73 | currentLessonIndex: 0, 74 | currentStepIndex: 0 75 | } 76 | } 77 | return update(state, { 78 | lessons: {$push: [action.imported]} 79 | }) 80 | case types.TOGGLE_EDITING: 81 | return {...state, editing: action.editing} 82 | case types.SELECT_LESSON: 83 | code = state.lessons[action.lessonIndex].steps[0].code 84 | return { 85 | ...state, 86 | code, 87 | currentLessonIndex: action.lessonIndex, 88 | currentStepIndex: 0, 89 | executedCode: code 90 | } 91 | case types.SELECT_STEP: 92 | code = state.lessons[state.currentLessonIndex].steps[action.stepIndex].code 93 | return { 94 | ...state, 95 | code, 96 | currentStepIndex: action.stepIndex, 97 | executedCode: code 98 | } 99 | case types.UPDATE_CODE: 100 | return {...state, code: action.code} 101 | case types.UPDATE_LESSON: 102 | return update(state, { 103 | lessons: { 104 | [state.currentLessonIndex]: {$merge: action.update} 105 | } 106 | }) 107 | case types.UPDATE_STEP: 108 | return update(state, { 109 | lessons: { 110 | [state.currentLessonIndex]: { 111 | steps: { 112 | [state.currentStepIndex]: {$merge: action.update} 113 | } 114 | } 115 | } 116 | }) 117 | } 118 | return state 119 | } 120 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | var ADD_LESSON = 'ADD_LESSON' 2 | var ADD_STEP = 'ADD_STEP' 3 | var DELETE_LESSON = 'DELETE_LESSON' 4 | var DELETE_STEP = 'DELETE_STEP' 5 | var EXECUTE_CODE = 'EXECUTE_CODE' 6 | var IMPORT_LESSONS = 'IMPORT_LESSONS' 7 | var SELECT_LESSON = 'SELECT_LESSON' 8 | var SELECT_STEP = 'SELECT_STEP' 9 | var TOGGLE_EDITING = 'TOGGLE_EDITING' 10 | var UPDATE_CODE = 'UPDATE_CODE' 11 | var UPDATE_LESSON = 'UPDATE_LESSON' 12 | var UPDATE_STEP = 'UPDATE_STEP' 13 | 14 | module.exports = { 15 | ADD_LESSON, 16 | ADD_STEP, 17 | DELETE_LESSON, 18 | DELETE_STEP, 19 | EXECUTE_CODE, 20 | IMPORT_LESSONS, 21 | SELECT_LESSON, 22 | SELECT_STEP, 23 | TOGGLE_EDITING, 24 | UPDATE_CODE, 25 | UPDATE_LESSON, 26 | UPDATE_STEP 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/codemirror-jsx.js: -------------------------------------------------------------------------------- 1 | // From https://gist.github.com/dzannotti/9a0efaf04072069fa63b 2 | 3 | var CodeMirror = require('codemirror') 4 | 5 | CodeMirror.defineMode('jsx', function(config) { 6 | return CodeMirror.multiplexingMode( 7 | CodeMirror.getMode(config, 'javascript'), { 8 | open: '<', close: '>', 9 | mode: CodeMirror.multiplexingMode( 10 | CodeMirror.getMode(config, {name: 'xml', htmlMode: true}), { 11 | open: '{', close: '}', 12 | mode: CodeMirror.getMode(config, 'javascript'), 13 | parseDelimiters: false 14 | } 15 | ), 16 | parseDelimiters: true 17 | }) 18 | }) 19 | 20 | CodeMirror.defineMIME('text/jsx', 'jsx') 21 | -------------------------------------------------------------------------------- /src/utils/export-json.js: -------------------------------------------------------------------------------- 1 | var Base64 = require('base-64') 2 | 3 | function exportJSON(lessons, filename='export.json') { 4 | var json = JSON.stringify(lessons, null, 2) 5 | var json64 = Base64.encode(unescape(encodeURIComponent(json))) 6 | var a = document.createElement('a') 7 | if ('download' in a) { 8 | a.href = `data:text/json;base64,${json64}` 9 | a.download = filename 10 | var event = document.createEvent('MouseEvents') 11 | event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, 12 | false, false, false, false, 0, null) 13 | a.dispatchEvent(event) 14 | } 15 | else if (typeof navigator.msSaveBlob == 'function') { 16 | navigator.msSaveBlob(new window.Blob([json], { 17 | type: 'text/json;charset=utf-8;' 18 | }), filename) 19 | } 20 | else { 21 | window.location.href = `data:application/octet-stream;base64,${json64}` 22 | } 23 | } 24 | 25 | module.exports = exportJSON 26 | -------------------------------------------------------------------------------- /src/utils/parse-json-file.js: -------------------------------------------------------------------------------- 1 | function parseJSONFile(file, cb) { 2 | var reader = new window.FileReader() 3 | reader.onload = (e) => { 4 | var json = e.target.result 5 | try { 6 | cb(null, JSON.parse(json)) 7 | } 8 | catch (e) { 9 | cb(e) 10 | } 11 | } 12 | reader.readAsText(file) 13 | } 14 | 15 | module.exports = parseJSONFile 16 | -------------------------------------------------------------------------------- /src/utils/uuid.js: -------------------------------------------------------------------------------- 1 | function uuid() { 2 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { 3 | var r = Math.random() * 16 | 0 4 | var v = c === 'x' ? r : (r & 0x3 | 0x8) 5 | return v.toString(16) 6 | }) 7 | } 8 | 9 | module.exports = uuid 10 | -------------------------------------------------------------------------------- /webpack.build.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | var webpack = require('webpack') 5 | 6 | module.exports = { 7 | entry: { 8 | app: './src/index', 9 | vendor: [ 10 | 'babel-core/browser', 11 | 'base-64', 12 | 'boxxy', 13 | 'classnames', 14 | 'codemirror', 15 | 'codemirror/addon/mode/overlay', 16 | 'codemirror/addon/mode/multiplex', 17 | 'codemirror/mode/javascript/javascript', 18 | 'codemirror/mode/markdown/markdown', 19 | 'codemirror/mode/xml/xml', 20 | 'codemirror/mode/gfm/gfm', 21 | 'marked', 22 | 'react', 23 | 'react-router', 24 | 'react-codemirror', 25 | 'redux' 26 | ] 27 | }, 28 | node: { 29 | buffer: false, 30 | global: false, 31 | process: false 32 | }, 33 | output: { 34 | path: 'public', 35 | filename: 'app.js' 36 | }, 37 | plugins: [ 38 | new webpack.NoErrorsPlugin(), 39 | new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.stringify('production')}), 40 | new ExtractTextPlugin('style.css'), 41 | new webpack.optimize.CommonsChunkPlugin('vendor', 'deps.js'), 42 | new webpack.optimize.OccurenceOrderPlugin(), 43 | new webpack.optimize.DedupePlugin(), 44 | new webpack.optimize.UglifyJsPlugin({ 45 | compressor: { 46 | warnings: false 47 | } 48 | }) 49 | ], 50 | module: { 51 | loaders: [ 52 | {test: /\.js$/, loader: 'babel', exclude: /node_modules/}, 53 | {test: /\.json$/, loader: 'json'}, 54 | {test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css?-restructuring!autoprefixer')} 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var path = require('path') 4 | var webpack = require('webpack') 5 | 6 | module.exports = { 7 | devtool: 'eval', 8 | entry: [ 9 | 'webpack-dev-server/client?http://localhost:3000', 10 | 'webpack/hot/only-dev-server', 11 | './src/index' 12 | ], 13 | output: { 14 | path: path.join(__dirname, 'build'), 15 | filename: 'bundle.js', 16 | publicPath: '/' 17 | }, 18 | plugins: [ 19 | new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.stringify('development')}), 20 | new webpack.HotModuleReplacementPlugin(), 21 | new webpack.NoErrorsPlugin() 22 | ], 23 | resolve: { 24 | extensions: ['', '.js', '.json'] 25 | }, 26 | module: { 27 | loaders: [ 28 | {test: /\.js$/, loader: 'react-hot!babel', include: path.join(__dirname, 'src')}, 29 | {test: /\.json$/, loader: 'json'}, 30 | {test: /\.css$/, loader: 'style!css?-restructuring!autoprefixer'} 31 | ] 32 | } 33 | } 34 | --------------------------------------------------------------------------------