├── .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 | 
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 | 
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 |
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 |
--------------------------------------------------------------------------------