├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── example-devel ├── gulpfile.js ├── package.json └── src ├── ace.jsx ├── app.jsx ├── auth.jsx ├── ckeditor.jsx ├── common.jsx ├── contents.jsx ├── delete.jsx ├── demo.jsx ├── editor.jsx ├── errors.jsx ├── file.jsx ├── github.jsx ├── home.jsx ├── index.html ├── initial.jsx ├── navbar.jsx ├── newfile.jsx ├── newrepo.jsx ├── newsite.jsx ├── site.jsx ├── style.css ├── track.jsx └── verify.jsx /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /devel 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Hacking on JekyllCMS 2 | 3 | Check out the [Trello board](https://trello.com/b/F1UQejYU/jekyllcms), that's 4 | where we keep track of things to do. The green label means "beginner-friendly 5 | task" and red means "high-value task". 6 | 7 | 1. Clone repo and install npm dependencies 8 | ``` 9 | $ git clone https://github.com/mgax/jekyllcms.git 10 | $ cd jekyllcms 11 | $ npm install 12 | $ npm install -g gulp 13 | ``` 14 | 15 | 2. Create GitHub Application - go to 16 | [Developer applications](https://github.com/settings/developers), click 17 | "Register new application", and fill in the details: 18 | 19 | * *Application name*: `JekyllCMS development` 20 | * *Homepage URL*: `http://localhost:5000/` 21 | * *Authorization callback URL*: `http://localhost:5000/` 22 | 23 | 3. Copy the file `example-devel` to `devel` and set `GITHUB_OAUTH_KEY` and 24 | `GITHUB_OAUTH_SECRET` from the GitHub application you just created. 25 | 26 | 4. Run the project (`./devel`) and open it in your web browser 27 | (`http://localhost:5000/`) 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alex Morega 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [JekyllCMS](https://jekyllcms.grep.ro/) is a content editor for [GitHub 2 | Pages][gh-pages]. Its purpose is to let non-programmers manage content in a 3 | GitHub Pages website. It's not an end-to-end CMS, a technical person still 4 | needs to set up the website and its layout, but it makes day-to-day content 5 | editing painless. 6 | 7 | If you want to improve JekyllCMS, see the [contributor guide](CONTRIBUTING.md). 8 | 9 | [gh-pages]: https://pages.github.com/ 10 | -------------------------------------------------------------------------------- /example-devel: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PORT=5000 4 | export APP_URL='http://localhost:5000' 5 | export GITHUB_OAUTH_KEY='mygithubkey' 6 | export GITHUB_OAUTH_SECRET='mygithubsecret' 7 | export DROPBOX_KEY='mydropboxkey' 8 | 9 | exec gulp devel 10 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var gulp = require('gulp') 3 | var babel = require('gulp-babel') 4 | var concat = require('gulp-concat') 5 | var sourcemaps = require('gulp-sourcemaps') 6 | var Handlebars = require('handlebars') 7 | var https = require('https') 8 | var qs = require('querystring') 9 | var express = require('express') 10 | var path = require('path') 11 | 12 | var env = function(name, defaultValue) { 13 | var value = process.env[name] 14 | if(!value) { 15 | if(defaultValue !== undefined) { return defaultValue } 16 | throw(new Error("Missing " + name + " env variable")) 17 | } 18 | return value 19 | } 20 | 21 | gulp.task('js', function() { 22 | return gulp.src('src/*.jsx') 23 | .pipe(sourcemaps.init()) 24 | .pipe(babel()) 25 | .pipe(concat('app.js')) 26 | .pipe(sourcemaps.write('./')) 27 | .pipe(gulp.dest('build')) 28 | }) 29 | 30 | gulp.task('build', ['js'], function() { 31 | var template = function(name) { 32 | return Handlebars.compile(fs.readFileSync(name, {encoding: 'utf-8'})) 33 | } 34 | var index_html = template('src/index.html')({t: (new Date()).getTime()}) 35 | fs.writeFileSync('build/index.html', index_html) 36 | fs.writeFileSync('build/style.css', fs.readFileSync('src/style.css')) 37 | }) 38 | 39 | gulp.task('devel', ['build'], function() { 40 | gulp.watch('src/*.jsx', ['build']) 41 | server() 42 | }) 43 | 44 | gulp.task('default', ['build']) 45 | 46 | 47 | function authMiddleware() { 48 | // github oauth (code from github.com/prose/gatekeeper) 49 | 50 | var app = express() 51 | 52 | function authenticate(code, cb) { 53 | var data = qs.stringify({ 54 | client_id: env('GITHUB_OAUTH_KEY'), 55 | client_secret: env('GITHUB_OAUTH_SECRET'), 56 | code: code 57 | }) 58 | 59 | var reqOptions = { 60 | host: 'github.com', 61 | port: 443, 62 | path: '/login/oauth/access_token', 63 | method: 'POST', 64 | headers: {'content-length': data.length} 65 | } 66 | 67 | var body = '' 68 | var req = https.request(reqOptions, function(res) { 69 | res.setEncoding('utf8') 70 | res.on('data', function(chunk) { body += chunk; }) 71 | res.on('end', function() { 72 | cb(null, qs.parse(body).access_token) 73 | }) 74 | }) 75 | 76 | req.write(data) 77 | req.end() 78 | req.on('error', function(e) { cb(e.message); }) 79 | } 80 | 81 | app.get('/authenticate/:code', function(req, res) { 82 | console.log('authenticating code:' + req.params.code) 83 | authenticate(req.params.code, function(err, token) { 84 | var result = err || !token ? {'error': 'bad_code'} : {'token': token} 85 | console.log(result) 86 | res.json(result) 87 | }) 88 | }) 89 | 90 | return app 91 | } 92 | 93 | 94 | function server() { 95 | var app = express() 96 | app.use(authMiddleware()) 97 | app.use(express.static('build')) 98 | app.get('/config.json', function(req, res) { 99 | res.json({ 100 | "url": env('APP_URL'), 101 | "gatekeeper": env('APP_URL'), 102 | "clientId": env('GITHUB_OAUTH_KEY'), 103 | "dropboxKey": env('DROPBOX_KEY', ''), 104 | }) 105 | }) 106 | 107 | app.get('*', function(request, response) { 108 | response.sendFile(path.resolve(__dirname, 'build', 'index.html')) 109 | }) 110 | 111 | var port = +env('PORT', 9999) 112 | app.listen(port, null, function(err) { 113 | console.log('Gatekeeper, at your service: http://localhost:' + port) 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jekyllcms", 3 | "private": true, 4 | "devDependencies": { 5 | "express": "^4.13.3", 6 | "gulp": "^3.8.11", 7 | "gulp-babel": "^5.1.0", 8 | "gulp-concat": "^2.5.2", 9 | "gulp-sourcemaps": "^1.5.2", 10 | "handlebars": "^3.0.3" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ace.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Ace extends React.Component { 4 | render() { 5 | return
; 6 | } 7 | componentDidMount() { 8 | this.reset(this.props.initial); 9 | } 10 | reset(content) { 11 | if(this.ace) { this.ace.destroy(); } 12 | var node = React.findDOMNode(this.refs.ace); 13 | $(node).text(content); 14 | this.ace = ace.edit(node); 15 | this.ace.getSession().setMode('ace/mode/markdown'); 16 | this.ace.setTheme('ace/theme/github'); 17 | this.ace.setShowPrintMargin(false); 18 | this.ace.setHighlightActiveLine(false); 19 | this.ace.on('change', (e) => this.handleChange(e)); 20 | } 21 | handleChange() { 22 | this.props.onChange(this.ace.getValue()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class App extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = {view: null}; 7 | } 8 | componentWillMount() { 9 | window.app = this; 10 | } 11 | render() { 12 | var view = this.props.children; 13 | let { query } = this.props.location; 14 | 15 | if(query.code) { 16 | view = ; 17 | } else { 18 | this.authToken = localStorage.getItem('jekyllcms-github-token'); 19 | if(!this.authToken & !query.demo){ 20 | view = ; 21 | } 22 | } 23 | 24 | if (!view) { 25 | let github = GitHub.create(); 26 | if (github) { 27 | github.authenticatedUserLogin().then((userName) => { 28 | this.props.history.pushState(null, '/' + userName); 29 | }); 30 | } 31 | } 32 | 33 | return ( 34 |
35 |
36 | 37 |
38 | {view} 39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 | ); 48 | } 49 | componentDidMount() { 50 | $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', () => { 51 | React.unmountComponentAtNode(React.findDOMNode(this.refs.modalDialog)); 52 | }); 53 | } 54 | modal(component) { 55 | React.render(component, React.findDOMNode(this.refs.modalDialog)); 56 | $(React.findDOMNode(this.refs.modal)).modal(); 57 | } 58 | hideModal() { 59 | $(React.findDOMNode(this.refs.modal)).modal('hide'); 60 | } 61 | reportError(message) { 62 | this.refs.errorBox.report(message); 63 | } 64 | } 65 | 66 | $.get('/config.json', (config) => { 67 | let Router = ReactRouter.Router; 68 | let Route = ReactRouter.Route; 69 | let browserHistory = ReactRouter.browserHistory; 70 | 71 | var query = parseQuery(window.location.search); 72 | if(config.piwik && ! query['code']) { 73 | trackPiwik(config.piwik); 74 | } 75 | window.__app_config = config; 76 | React.render( 77 | 78 | 79 | 80 | 81 | 82 | 83 | , 84 | document.querySelector('body') 85 | ); 86 | }); 87 | -------------------------------------------------------------------------------- /src/auth.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Authorize extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = {demo: false}; 7 | } 8 | render() { 9 | var authUrl = 'https://github.com/login/oauth/authorize' + 10 | '?client_id=' + encodeURIComponent(__app_config.clientId) + 11 | '&scope=user:email,public_repo' + 12 | '&redirect_uri=' + encodeURIComponent(__app_config.url); 13 | return ( 14 |
15 |
16 |

17 | JekyllCMS is a content editor 18 | for GitHub Pages websites. 19 |

20 |

21 | 22 | Learn more 23 | 24 |

25 |

26 | 27 | Authorize on GitHub 28 | 29 |

30 |

31 | Or enter demo mode, 32 | which requires no authorization, but is read-only. 33 |

34 | {this.state.demo ? 35 |
36 | 40 | 43 |
44 | : ''} 45 |
46 |
47 | ); 48 | } 49 | handleDemo(evt) { 50 | evt.preventDefault(); 51 | this.setState({demo: true}); 52 | } 53 | handleBrowse(evt) { 54 | evt.preventDefault(); 55 | var login = React.findDOMNode(this.refs.account).value; 56 | window.location.href = '/' + encodeURIComponent(login) + '?demo=on'; 57 | } 58 | } 59 | 60 | class AuthCallback extends React.Component { 61 | render() { 62 | return

Saving authorization token...

; 63 | } 64 | componentDidMount() { 65 | $.get(__app_config.gatekeeper + '/authenticate/' + this.props.code, (resp) => { 66 | if(resp.token) { 67 | localStorage.setItem('jekyllcms-github-token', resp.token); 68 | window.location.href = '/'; 69 | } 70 | }); 71 | } 72 | } 73 | 74 | class LogoutButton extends React.Component { 75 | render() { 76 | return ( 77 | 81 | ); 82 | } 83 | handleClick() { 84 | localStorage.removeItem('jekyllcms-github-token'); 85 | window.location.href = '/'; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/ckeditor.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class CKEditor extends React.Component { 4 | render() { 5 | return