├── .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 |
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 |
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 | logout
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 ;
6 | }
7 | componentDidMount() {
8 | this.reset(this.props.initial);
9 | }
10 | componentWillUnmount() {
11 | if(this.ck) { this.ck.destroy(); }
12 | }
13 | getSiteUrl() {
14 | return 'http://' + this.props.config.siteUrl;
15 | }
16 | toHtml(source) {
17 | return (source
18 | .split(/{{\s*site\.base_url\s*}}/)
19 | .join(this.getSiteUrl())
20 | );
21 | }
22 | fromHtml(html) {
23 | return (html
24 | .split(this.getSiteUrl())
25 | .join('{{ site.base_url }}')
26 | );
27 | }
28 | reset(content) {
29 | if(this.ck) { this.ck.destroy(); }
30 | var node = React.findDOMNode(this.refs.ck);
31 | $(node).text(this.toHtml(content));
32 |
33 | var imageBrowserUrl = window.location.pathname + '/ckImageBrowser'
34 | + window.location.search + (window.location.search ? '&' : '?')
35 | + 'siteUrl=' + encodeURIComponent(this.getSiteUrl());
36 | var options = {
37 | allowedContent: true,
38 | filebrowserImageBrowseUrl: imageBrowserUrl,
39 | toolbar: [
40 | {name: 'styles', items: ['Format']},
41 | {name: 'basicstyles', items: ['Bold', 'Italic', 'Underline', 'Strike']},
42 | {name: 'paragraph', items: ['NumberedList', 'BulletedList', 'Indent', 'Outdent', '-', 'Blockquote']},
43 | {name: 'table', items: ['Table', 'Image']},
44 | {name: 'links', items: ['Link', 'Unlink', '-', 'Anchor']},
45 | {name: 'subsuper', items: ['Subscript', 'Superscript']},
46 | {name: 'colors', items: ['TextColor', 'BGColor']},
47 | {name: 'tools', items: ['SpecialChar', '-', 'RemoveFormat', 'Maximize', '-', 'Source']}
48 | ]
49 | };
50 | this.ck = CKEDITOR.replace(node, options);
51 | this.ck.on('change', () => this.handleChange());
52 | }
53 | handleChange() {
54 | this.props.onChange(this.fromHtml(this.ck.getData()));
55 | }
56 | }
57 |
58 | class CKImageBrowser extends React.Component {
59 | constructor(props) {
60 | super(props);
61 | this.state = {};
62 | }
63 | render() {
64 | var imageLi = (file) => {
65 | if(file.path.match(/.+\.(jp[e]?g|png|gif)$/)) {
66 | return (
67 |
68 |
69 | {file.path}
70 |
71 |
72 |
73 |
74 | );
75 | }
76 | };
77 | var contents = Loading files...
;
78 | if(this.state.media) {
79 | contents = (
80 |
81 | {this.state.media.map(imageLi)}
82 |
83 | );
84 | }
85 | return (
86 |
87 |
88 | {contents}
89 |
90 | )
91 | }
92 | componentWillMount() {
93 | $('head').append('');
96 | }
97 | componentDidMount() {
98 | waitFor(() => !! window.Dropbox, () => {
99 | var button = Dropbox.createChooseButton({
100 | success: (files) => {
101 | var url = files[0].link.split('?')[0] + '?raw=1';
102 | this.chooseImage(url);
103 | }
104 | });
105 | React.findDOMNode(this.refs.dropbox).appendChild(button);
106 | });
107 | var branch = this.props.repo.branch(this.props.branchName);
108 | branch.files()
109 | .then((tree) => {
110 | var media = tree.filter((f) => f.path.startsWith('media/'));
111 | this.setState({media: media});
112 | })
113 | .catch(errorHandler("loading file list"));
114 | }
115 | handleClick(path, evt) {
116 | evt.preventDefault();
117 | var site = window.opener.app.refs.site;
118 | var url = this.props.siteUrl + '/' + path;
119 | this.chooseImage(url);
120 | }
121 | chooseImage(url) {
122 | window.opener.CKEDITOR.tools.callFunction(this.props.funcNum, url);
123 | window.close();
124 | }
125 | }
126 |
127 | class CKImageBrowserWrapper extends React.Component {
128 | constructor(props) {
129 | super(props);
130 | this.state = {};
131 | }
132 | componentDidMount() {
133 | let {query} = this.props.location;
134 | let {userName, repoName} = this.props.params;
135 | let gitHub = GitHub.create(query.demo);
136 | return gitHub.repo(userName + '/' + repoName)
137 | .then((repo) => {
138 | this.setState({
139 | repo: repo,
140 | branchName: query['branch'] ? query['branch'] : repo.getDefaultBranchName()
141 | });
142 | })
143 | .catch(errorHandler("loading repository"));
144 | }
145 | render() {
146 | let {query} = this.props.location;
147 | let {branchName, repo} = this.state;
148 | if (!branchName) {
149 | return false
150 | }
151 | console.log(this.state, branchName, repo, query);
152 | return (
153 |
154 |
160 |
161 |
);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/common.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Q.longStackSupport = true;
4 |
5 | function assert(cond) {
6 | if(! cond) {
7 | throw "assertion failed";
8 | }
9 | }
10 |
11 | function encode_utf8(s) {
12 | return unescape(encodeURIComponent(s));
13 | }
14 |
15 | function decode_utf8(s) {
16 | return decodeURIComponent(escape(s));
17 | }
18 |
19 | function reportError(e, action) {
20 | var message = "Error";
21 | if(action) {
22 | message += " while " + action;
23 | }
24 |
25 | if(e.readyState !== null) { // ajax error
26 | var cause = (e.responseJSON && e.responseJSON.message);
27 | if(cause) {
28 | message = message + ": " + cause;
29 | }
30 | }
31 |
32 | console.error(message, e, e.stack);
33 | app.reportError(message);
34 | }
35 |
36 | function errorHandler(action) {
37 | return function(e) {
38 | reportError(e, action);
39 | }
40 | }
41 |
42 | function parseQuery(url) {
43 | var rv = {};
44 | if(url.indexOf('?') > -1) {
45 | url.match(/\?(.*)/)[1].split('&').forEach((pair) => {
46 | var [k, v] = pair.split('=').map(decodeURIComponent);
47 | if(! rv[k]) { rv[k] = []; }
48 | rv[k].push(v);
49 | });
50 | }
51 | return rv;
52 | }
53 |
54 | function clone(value) {
55 | return JSON.parse(JSON.stringify(value));
56 | }
57 |
58 | function slugify(text) {
59 | var romap = {
60 | 'ă': 'a',
61 | 'Ă': 'A',
62 | 'î': 'i',
63 | 'Î': 'I',
64 | 'â': 'a',
65 | 'Â': 'A',
66 | 'ș': 's',
67 | 'Ș': 'S',
68 | 'ş': 's',
69 | 'Ş': 'S',
70 | 'ț': 't',
71 | 'Ț': 'T',
72 | 'ţ': 't',
73 | 'Ţ': 'T',
74 | };
75 | return getSlug(
76 | text
77 | .replace(/_/g, ' ')
78 | .replace(/[șşțţăîâ]/ig, (ch) => romap[ch])
79 | );
80 | }
81 |
82 | function generateUnique(build, exists) {
83 | var value = build('');
84 | if(! exists(value)) { return value; }
85 | var n = 0;
86 | while(n < 100) {
87 | n += 1;
88 | value = build('-' + n);
89 | if(! exists(value)) { return value; }
90 | }
91 | throw new Error("Counld not generate unique name, stopping at " + value);
92 | }
93 |
94 | function waitFor(condition, callback) {
95 | var interval = setInterval(() => {
96 | if(condition()) {
97 | clearInterval(interval);
98 | callback();
99 | }
100 | }, 100);
101 | }
102 |
--------------------------------------------------------------------------------
/src/contents.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class FileView extends React.Component {
4 | render() {
5 | var cls = ['file'];
6 | if(this.props.current) { cls.push('current'); }
7 | if(this.props.file.isNew()) { cls.push('new'); }
8 | return (
9 |
10 | {this.props.file.permalink(true)}
15 |
16 | );
17 | }
18 | handleClick(evt) {
19 | evt.preventDefault();
20 | this.props.onEdit(this.props.file);
21 | }
22 | }
23 |
24 | class CollectionView extends React.Component {
25 | render() {
26 | var fileViews = this.props.collection.files
27 | .filter((file) => file.path.match(/\.(md|markdown|html)$/))
28 | .sort((a, b) => a.path < b.path ? -1 : 1)
29 | .map((file) =>
30 |
36 | );
37 | return (
38 |
39 |
40 | {this.props.name}{' '}
41 |
42 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | handleCreateDialog() {
54 | app.modal(
55 | this.props.onCreate(options, this.props.collection)}
57 | pathExists={this.props.pathExists}
58 | config={this.props.config}
59 | collection={this.props.collection}
60 | />
61 | );
62 | }
63 | }
64 |
65 | function extractCollections(tree, config) {
66 | var collections = {};
67 | var colList = ['posts', 'pages'].map((name) => {
68 | var col = new Collection(name, config.permalinkTemplate(name));
69 | collections[col.name] = col;
70 | return col;
71 | });
72 |
73 | tree.forEach((ghFile) => {
74 | for(var i = 0; i < colList.length; i ++) {
75 | var col = colList[i];
76 | var file = col.match(ghFile);
77 | if(file) {
78 | col.files.push(file);
79 | break;
80 | }
81 | }
82 | });
83 |
84 | return collections;
85 | }
86 |
87 | class SiteContents extends React.Component {
88 | constructor(props) {
89 | super(props);
90 | this.state = {collections: extractCollections(props.tree, props.config)};
91 | }
92 | componentWillReceiveProps(props) {
93 | this.setState({collections: extractCollections(props.tree, props.config)});
94 | }
95 | render() {
96 | var collectionViews = Object.keys(this.state.collections)
97 | .sort()
98 | .map((name) =>
99 |
109 | );
110 |
111 | var editor = null;
112 | if(this.state.currentFile) {
113 | editor = (
114 |
126 | );
127 | }
128 |
129 | return (
130 |
131 | {collectionViews}
132 | {editor}
133 |
134 | );
135 | }
136 | pathExists(path) {
137 | var matching = this.props.tree.filter((f) => f.path == path);
138 | return matching.length > 0;
139 | }
140 | handleCreate(options, collection) {
141 | var file = collection.match(this.props.createFile(options.path), true);
142 | file.initialTitle = options.title;
143 | this.setState({currentFile: file});
144 | }
145 | handleEdit(file) {
146 | this.setState({currentFile: file});
147 | }
148 | handleDelete() {
149 | this.setState({currentFile: null});
150 | this.props.onTreeChange();
151 | }
152 | handleClose() {
153 | this.setState({currentFile: null});
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/delete.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class DeleteFileModal extends React.Component {
4 | render() {
5 | return (
6 |
7 |
8 |
9 | ×
10 |
11 |
Delete file
12 |
13 |
14 |
Are you sure you want to delete {this.props.path} ?
15 |
16 |
17 |
18 | Cancel
19 |
20 | Delete
24 |
25 |
26 | );
27 | }
28 | handleDelete() {
29 | app.hideModal();
30 | this.props.onDelete();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/demo.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class Demo extends React.Component {
4 | render() {
5 | var account = this.props.account;
6 | return (
7 |
8 |
{account.meta.login} repositories
9 |
10 |
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/editor.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class FrontMatterField extends React.Component {
4 | render() {
5 | return (
6 |
12 | );
13 | }
14 | handleChange(evt) {
15 | this.props.onChange(evt.target.value);
16 | }
17 | }
18 |
19 | class FrontMatter extends React.Component {
20 | constructor(props) {
21 | super(props);
22 | this.state = {data: clone(this.props.data)};
23 | }
24 | render() {
25 | return ;
30 | }
31 | handleChange(name, value) {
32 | var newData = clone(this.state.data);
33 | newData[name] = value;
34 | this.setState({data: newData});
35 | this.props.onChange(newData);
36 | }
37 | getData() {
38 | return this.state.data;
39 | }
40 | }
41 |
42 | class SaveButton extends React.Component {
43 | constructor(props) {
44 | super(props);
45 | this.state = {state: 'ready'};
46 | }
47 | render() {
48 | var text = "save";
49 | if(this.state.state == 'success') { text += " ✔"; }
50 | if(this.state.state == 'error') { text += " - error!"; }
51 | if(this.state.state == 'saving') { text += " ..."; }
52 | return (
53 | {text}
58 | );
59 | }
60 | handleSave() {
61 | this.setState({state: 'saving'});
62 | this.props.onSave()
63 | .then(() => {
64 | this.setState({state: 'success'});
65 | })
66 | .catch((e) => {
67 | this.setState({state: 'error'});
68 | reportError(e, "saving file");
69 | });
70 | }
71 | }
72 |
73 | class DeleteButton extends React.Component {
74 | render() {
75 | return (
76 | delete
81 | )
82 | }
83 | handleClick() {
84 | app.modal(
85 |
89 | );
90 | }
91 | handleDelete() {
92 | if(! this.props.file.isNew()) {
93 | this.props.file.delete()
94 | .then(() => {
95 | this.props.onDelete();
96 | })
97 | .catch(errorHandler("deleting file"));
98 | }
99 | else {
100 | this.props.onDelete();
101 | }
102 | }
103 | }
104 |
105 | class Editor extends React.Component {
106 | constructor(props) {
107 | super(props);
108 | this.state = {loading: true};
109 | }
110 | render() {
111 | var path = this.props.file.path;
112 | var closebtn = (
113 |
114 | ×
118 |
119 | );
120 | var permalink = this.props.file.permalink(true);
121 | var publicUrl = 'http://' + this.props.config.siteUrl + permalink;
122 | if(publicUrl.match(/[^\/]$/)) {
123 | publicUrl += '.html';
124 | }
125 | var title = (
126 |
127 | {permalink} {' '}
128 |
129 |
130 |
131 |
132 | );
133 | if(this.state.loading) {
134 | return (
135 |
136 | {closebtn}
137 | {title}
138 |
loading {path} ...
139 |
140 | );
141 | }
142 | else {
143 | var html = marked(this.state.content, {sanitize: true});
144 | if(! this.state.frontMatter) {
145 | editor = (
146 |
147 |
148 | This file has no{' '}
149 |
150 | front matter
151 | , showing raw content.
152 |
153 |
154 |
155 | {this.state.content}
156 |
157 |
158 |
159 | );
160 | }
161 | else if(path.match(/\.html/)) {
162 | var editor = (
163 |
164 |
167 |
173 |
174 | );
175 | var preview = null;
176 | }
177 | else {
178 | var markdownUrl = 'https://help.github.com/articles/markdown-basics/';
179 | var editor = (
180 |
181 |
184 |
185 | This page is written using{' '}
186 | Markdown .
187 | As you type, a preview is shown below.
188 |
189 |
194 |
195 | );
196 | var preview = (
197 |
201 | );
202 | }
203 | return (
204 |
205 | {closebtn}
206 | {title}
207 | {editor}
208 | {preview}
209 |
210 |
214 |
215 |
220 |
221 |
222 | );
223 | }
224 | }
225 | componentDidMount() {
226 | this.loadFile(this.props.file);
227 | }
228 | componentWillReceiveProps(newProps) {
229 | this.setState({
230 | loading: true,
231 | frontMatter: null,
232 | content: null,
233 | });
234 | this.loadFile(newProps.file);
235 | }
236 | loadFile(file) {
237 | file.load()
238 | .then((data) => {
239 | if(! this.state.loading) {
240 | this.refs.contentEditor.reset(data.content);
241 | }
242 | this.setState({
243 | loading: false,
244 | frontMatter: data.frontMatter,
245 | content: data.content,
246 | });
247 | })
248 | .catch(errorHandler("loading file contents"));
249 | }
250 | handleChange(content) {
251 | this.setState({content: content});
252 | }
253 | handleFrontMatterChange(frontMatter) {
254 | this.setState({frontMatter: frontMatter});
255 | }
256 | handleSave() {
257 | return this.props.file
258 | .save({
259 | frontMatter: this.state.frontMatter,
260 | content: this.state.content,
261 | });
262 | }
263 | handleDelete() {
264 | this.props.onDelete();
265 | }
266 | handleClose() {
267 | this.props.onClose();
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/src/errors.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | class ErrorMessage extends React.Component {
5 | render() {
6 | return (
7 |
8 | ×
11 | {this.props.text}
12 |
13 | );
14 | }
15 | }
16 |
17 |
18 | class ErrorBox extends React.Component {
19 | constructor(props) {
20 | super(props);
21 | this.messageList = [];
22 | this.nextId = 1;
23 | this.state = {messageList: this.messageList.slice()};
24 | }
25 | render() {
26 | return (
27 |
28 | {this.state.messageList.map((msg) =>
29 |
34 | )}
35 |
36 | );
37 | }
38 | report(text) {
39 | this.messageList.push({id: (this.nextId ++), text: text});
40 | this.setState({messageList: this.messageList});
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/file.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function matchPath(path, colName) {
4 | if(path.match(/\/[_.]/)) {
5 | // ignore paths that contain a non-root item that start with [_.]
6 | return;
7 | }
8 |
9 | if(colName == 'posts') {
10 | var m = path.match(/^_posts\/(\d{4})-(\d{2})-(\d{2})-(.+)\.[^\.]+$/);
11 | if(m) {
12 | var year = m[1];
13 | var month = m[2];
14 | var day = m[3];
15 | var title = m[4];
16 | return {
17 | year: year,
18 | month: month,
19 | i_month: +month,
20 | day: day,
21 | i_day: +day,
22 | short_year: year.slice(2),
23 | title: title,
24 | categories: '',
25 | output_ext: '.html',
26 | };
27 | }
28 | return;
29 | }
30 |
31 | if(colName == 'pages') {
32 | if(path.match(/^[_.]/)) {
33 | // ignore paths whose root item starts with [_.]
34 | return;
35 | }
36 |
37 | var m = path.match(/^(.*\/)?([^\/]*)\.[^\.]+$/);
38 | if(m) {
39 | return {
40 | path: m[1] || '',
41 | basename: m[2],
42 | output_ext: '.html',
43 | };
44 | }
45 | return;
46 | }
47 | }
48 |
49 | class File {
50 | constructor(ghFile, collection, permalinkVars) {
51 | this.ghFile = ghFile;
52 | this.path = ghFile.path;
53 | this.collection = collection;
54 | this.permalinkVars = permalinkVars;
55 | }
56 |
57 | isNew() {
58 | return ! this.ghFile.sha;
59 | }
60 |
61 | load() {
62 | function parse(src) {
63 | var lines = src.split(/\n/);
64 | var frontMatterStart = lines.indexOf('---');
65 | if(frontMatterStart != 0) {
66 | return {
67 | frontMatter: null,
68 | content: src
69 | }
70 | }
71 | var frontMatterEnd = lines.indexOf('---', frontMatterStart + 1);
72 | assert(frontMatterEnd > -1);
73 | return {
74 | frontMatter: jsyaml.safeLoad(
75 | lines.slice(frontMatterStart + 1, frontMatterEnd).join('\n') + '\n'
76 | ),
77 | content: lines.slice(frontMatterEnd + 1).join('\n')
78 | }
79 | }
80 |
81 | var getContent = (this.isNew()
82 | ? Q(encode_utf8(`---\ntitle: ${JSON.stringify(this.initialTitle)}\n---\n`))
83 | : this.ghFile.getContent());
84 | return getContent
85 | .then((content) => parse(decode_utf8(content)));
86 | }
87 |
88 | save(data) {
89 | var content = encode_utf8(
90 | '---\n' +
91 | jsyaml.safeDump(data.frontMatter) +
92 | '---\n' +
93 | data.content
94 | );
95 | return this.ghFile.putContent(content);
96 | }
97 |
98 | delete() {
99 | return this.ghFile.delete();
100 | }
101 |
102 | permalink(extensionless) {
103 | var rv = this.collection.permalinkTemplate
104 | .replace(/:(\w+)/g, (_, name) => this.permalinkVars[name] || '')
105 | .replace(/\/{2,}/g, '/')
106 | .replace(/\/index.html$/, '/');
107 | if(extensionless) {
108 | rv = rv.replace(/\.html$/, '');
109 | }
110 | return rv;
111 | }
112 |
113 | url() {
114 | return this.state.siteUrl + '/' + this.url();
115 | }
116 | }
117 |
118 | class Collection {
119 | constructor(name, permalinkTemplate) {
120 | this.name = name;
121 | this.permalinkTemplate = permalinkTemplate;
122 | this.files = [];
123 | }
124 |
125 | match(ghFile, errorOnFailure) {
126 | var permalinkVars = matchPath(ghFile.path, this.name);
127 | if(permalinkVars) {
128 | return new File(ghFile, this, permalinkVars);
129 | }
130 | else if(errorOnFailure) {
131 | throw new Error("Invalid path for " + this.name + ": " + ghFile.path);
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/github.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | class GitHubFile {
5 | constructor(branch, item) {
6 | this.branch = branch;
7 | this.path = item.path;
8 | this.sha = item.sha;
9 | }
10 |
11 | api(options) {
12 | return this.branch.repo.api(options);
13 | }
14 |
15 | getContent() {
16 | if(! this.sha) { return Q(''); }
17 | return this.api({url: '/git/blobs/' + this.sha})
18 | .then((resp) =>
19 | atob(resp.content.replace(/\s/g, '')));
20 | }
21 |
22 | putContent(newContent) {
23 | return this.api({
24 | url: '/contents/' + this.path,
25 | method: 'PUT',
26 | data: JSON.stringify({
27 | branch: this.branch.name,
28 | message: "Edit from JekyllCMS",
29 | path: this.path,
30 | sha: this.sha,
31 | content: btoa(newContent)
32 | })
33 | })
34 | .then((resp) => {
35 | this.sha = resp.content.sha;
36 | });
37 | }
38 |
39 | delete() {
40 | return this.api({
41 | url: '/contents/' + this.path,
42 | method: 'DELETE',
43 | data: JSON.stringify({
44 | branch: this.branch.name,
45 | message: "Edit from JekyllCMS",
46 | path: this.path,
47 | sha: this.sha,
48 | })
49 | });
50 | }
51 |
52 | contentUrl() {
53 | return 'https://raw.githubusercontent.com/'
54 | + this.branch.repo.meta.full_name
55 | + '/' + this.branch.name
56 | + '/' + this.path;
57 | }
58 | }
59 |
60 |
61 | class GitHubBranch {
62 | constructor(repo, name) {
63 | this.repo = repo;
64 | this.name = name;
65 | }
66 |
67 | files() {
68 | var url = '/git/trees/' + this.name + '?recursive=1';
69 | return this.repo.api({url: url, t: true})
70 | .then((resp) =>
71 | resp.tree
72 | .filter((i) => i.type == 'blob')
73 | .map((i) => new GitHubFile(this, i)));
74 | }
75 |
76 | newFile(path) {
77 | return new GitHubFile(this, {path: path});
78 | }
79 | }
80 |
81 |
82 | class GitHubRepo {
83 | constructor(gh, meta) {
84 | this.gh = gh;
85 | this.meta = meta;
86 | }
87 |
88 | api(options) {
89 | options.url = '/repos/' + this.meta.full_name + options.url;
90 | return this.gh.api(options);
91 | }
92 |
93 | branches() {
94 | return this.api({url: '/branches', t: true}).then((resp) =>
95 | resp.map((b) =>
96 | new GitHubBranch(this, b.name)));
97 | }
98 |
99 | branch(name) {
100 | return new GitHubBranch(this, name);
101 | }
102 |
103 | createBranch(name, fileList) {
104 | var branch = this.branch(name);
105 |
106 | var putFiles = (remaining) => {
107 | var file = remaining[0];
108 | if(! file) { return; }
109 |
110 | var githubFile = (new GitHubFile(branch, {path: file.path}));
111 | return githubFile.putContent(file.content)
112 | .then(() =>
113 | putFiles(remaining.slice(1)))
114 | };
115 |
116 | return putFiles(fileList);
117 | }
118 |
119 | getDefaultBranchName() {
120 | return (
121 | this.meta.name == this.meta.owner.login + '.github.com' ||
122 | this.meta.name == this.meta.owner.login + '.github.io'
123 | ? 'master'
124 | : 'gh-pages'
125 | );
126 | }
127 | }
128 |
129 |
130 | class GitHubAccount {
131 | constructor(gh, meta) {
132 | this.gh = gh;
133 | this.meta = meta;
134 | }
135 |
136 | repos() {
137 | var size = 100;
138 | var fetch = (n, rv) =>
139 | this.gh.api({
140 | url: this.meta.repos_url + '?per_page='+size+'&page='+n,
141 | t: true,
142 | })
143 | .then((resp) => {
144 | rv = rv.concat(resp);
145 | if(resp.length < size) { return rv; }
146 | else { return fetch(n+1, rv); }
147 | });
148 |
149 | return fetch(1, [])
150 | .then((repos) =>
151 | repos.map((meta) =>
152 | new GitHubRepo(this.gh, meta))
153 | );
154 | }
155 | }
156 |
157 |
158 | class GitHubUser {
159 | constructor(gh, meta) {
160 | this.gh = gh;
161 | this.meta = meta;
162 | this.account = new GitHubAccount(this.gh, this.meta);
163 | }
164 |
165 | orgs() {
166 | return this.gh.api({url: this.meta.organizations_url, t: true})
167 | .then((resp) =>
168 | resp.map((acc) =>
169 | new GitHubAccount(this.gh, acc)));
170 | }
171 |
172 | repos() {
173 | return this.account.repos();
174 | }
175 | }
176 |
177 | class GitHub {
178 | constructor(token) {
179 | this.token = token;
180 | }
181 |
182 | static create(demo) {
183 | var gitHub = null;
184 | if(demo) {
185 | return new GitHub();
186 | }
187 | else {
188 | let authToken = localStorage.getItem('jekyllcms-github-token');
189 | if(! authToken) {
190 | return null;
191 | }
192 | return new GitHub(authToken);// todo: add catch handlers
193 | }
194 | }
195 |
196 | api(options) {
197 | var apiUrl = 'https://api.github.com';
198 | if(options.url.indexOf(apiUrl) < 0) {
199 | options.url = apiUrl + options.url;
200 | }
201 | if(options.t) {
202 | var t = new Date().getTime();
203 | var sep = options.url.match(/\?/) ? '&' : '?';
204 | options.url += sep + 't=' + t;
205 | }
206 | if(this.token) {
207 | options.headers = {Authorization: 'token ' + this.token};
208 | }
209 | return Q($.ajax(options));
210 | }
211 |
212 | repo(fullName) {
213 | return this.api({url: '/repos/' + fullName, t: true})
214 | .then((meta) =>
215 | new GitHubRepo(this, meta));
216 | }
217 |
218 | user(login) {
219 | var url = (login ? '/users/' + login : '/user');
220 | return this.api({url: url, t: true})
221 | .then((meta) =>
222 | new GitHubUser(this, meta));
223 | }
224 |
225 | authenticatedUserLogin() {
226 | return this.api({url: '/user', t: true})
227 | .then((meta) => meta.login);
228 | }
229 |
230 | emailIsVerified() {
231 | return this.api({url: '/user/emails', t: true}).then((resp) =>
232 | resp.filter((i) => i.verified).length > 0);
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/src/home.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class Repo extends React.Component {
4 | render() {
5 | var Link = ReactRouter.Link;
6 | var repo = this.props.repo;
7 | var url = '/' + repo.meta.full_name;
8 | var query = {};
9 | if(this.props.demo) {
10 | query.demo = 'on';
11 | }
12 | return (
13 |
14 | {repo.meta.name}
15 | {repo.meta.description}
16 |
17 | );
18 | }
19 | }
20 |
21 | class Account extends React.Component {
22 | render() {
23 | var account = this.props.account;
24 | var caret = null;
25 | if(this.props.selected) {
26 | caret = »
;
27 | }
28 | return (
29 |
30 |
31 | {caret}
32 | {account.meta.login}
33 |
34 | );
35 | }
36 | handleClick(evt) {
37 | evt.preventDefault();
38 | this.props.onOpen(this.props.account);
39 | }
40 | }
41 |
42 | class AccountRepos extends React.Component {
43 | constructor(props) {
44 | super(props);
45 | this.state = {repoList: null};
46 | }
47 | render() {
48 | if(this.state.repoList) {
49 | var repoList = (this.state.repoList.length > 0
50 | ?
51 | {this.state.repoList.map((repo) =>
52 |
53 |
54 | )}
55 |
56 | :
57 | No repositories under{' '}
58 | {this.props.account.meta.login}
59 |
60 | );
61 | return (
62 |
63 |
64 | new
69 |
70 | {repoList}
71 |
72 | );
73 | }
74 | else {
75 | return (
76 |
77 | Loading
78 |
79 | );
80 | }
81 | }
82 | componentDidMount() {
83 | this.getRepos(this.props.account);
84 | }
85 | componentWillReceiveProps(newProps) {
86 | this.setState({repoList: null});
87 | this.getRepos(newProps.account);
88 | }
89 | getRepos(account) {
90 | account.repos()
91 | .then((repoList) => {
92 | repoList.sort((r1, r2) =>
93 | r1.meta.updated_at > r2.meta.updated_at ? -1 : 1);
94 | this.setState({repoList: repoList});
95 | })
96 | .catch(errorHandler("loading repository list"));
97 | }
98 | handleNew(evt) {
99 | app.modal(
100 | window.location.reload()}
103 | />
104 | );
105 | }
106 | }
107 |
108 | class Home extends React.Component {
109 | constructor(props) {
110 | super(props);
111 | this.state = {
112 | accountList: [],
113 | account: this.props.user,
114 | };
115 | }
116 | render() {
117 | return (
118 |
119 |
120 |
121 | {this.state.accountList.map((acc) =>
122 |
123 |
128 |
129 | )}
130 |
131 |
134 |
135 |
136 | );
137 | }
138 | componentDidMount() {
139 | this.props.user.orgs()
140 | .then((accountList) => {
141 | this.setState({accountList: [this.props.user].concat(accountList)});
142 | })
143 | .catch(errorHandler("loading organization list"));
144 | }
145 | handleOpen(account) {
146 | this.setState({account: account});
147 | }
148 | }
149 |
150 | function getUserPromise(demo, userName) {
151 | var gitHub = GitHub.create(demo);
152 | if (!gitHub) {
153 | return Q.reject('not authenticated');
154 | }
155 | return gitHub.user(userName)
156 | .catch((resp) => {
157 | if(resp.status == 401) {
158 | localStorage.removeItem('jekyllcms-github-token');``
159 | window.location.href = '/';
160 | return Q();
161 | }
162 | })
163 | .catch(errorHandler("loading user information"));
164 | }
165 |
166 | class HomeWrapper extends React.Component {
167 | constructor(props) {
168 | super(props);
169 | this.state = {};
170 | }
171 | componentDidMount() {
172 | let {query} = this.props.location;
173 | let {userName} = this.props.params;
174 | let userPromise = getUserPromise(query.demo, userName);
175 | return userPromise
176 | .then((account) => {
177 | this.setState({
178 | account: account
179 | });
180 | })
181 | .catch((err) => this.props.history.pushState(null, `/`));
182 | }
183 | render() {
184 | let {query} = this.props.location;
185 | let {userName} = this.props.params;
186 | let account = this.state.account;
187 | if (!account) {
188 | return false
189 | }
190 | if(query.demo) {
191 | return ;
192 | }
193 | return
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | JekyllCMS
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/initial.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function initialContent(options) {
4 |
5 | var gitignore = `\
6 | /_site
7 | `;
8 |
9 | var config_yml = `\
10 | title: ${JSON.stringify(options.title)}
11 | base_url: ${JSON.stringify('/'+options.repo)} # http://jekyllrb.com/docs/github-pages/#project-page-url-structure
12 | defaults:
13 | - scope: {path: ""}
14 | values:
15 | layout: "page"
16 | `;
17 |
18 | var layout_base_html = `\
19 |
20 |
21 | {{ page.title }} · {{ site.title }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
34 |
35 | {{ content }}
36 |
37 | `;
38 |
39 | var layout_home_html = `\
40 | ---
41 | layout: base
42 | ---
43 |
44 |
{{ site.title }}
45 | {{ content }}
46 |
47 |
48 | {% for post in site.posts %}
49 |
50 |
51 | {{ post.title}}
52 | {{ post.date | date_to_string }}
53 |
54 | {{ post.excerpt }}
55 |
56 | {% endfor %}
57 | `;
58 |
59 | var layout_page_html = `\
60 | ---
61 | layout: base
62 | ---
63 | {{ page.title }}
64 |
65 | {{ content }}
66 | `;
67 |
68 | var index_md = `\
69 | ---
70 | layout: home
71 | title: Home
72 | ---
73 | This site was generated using [JekyllCMS](http://jekyllcms.grep.ro/). It has a
74 | simple layout and some demo content to use as an example.
75 | `;
76 |
77 | var post_md = `\
78 | ---
79 | title: Today I created a website
80 | ---
81 | It was so easy!
82 |
83 | I just created a GitHub repo, went into JekyllCMS, entered a title, and poof! A
84 | website was born!
85 | `;
86 |
87 | var post_path = '_posts/' + moment().format('YYYY-MM-DD') + '-new-website.md';
88 |
89 | return [
90 | {path: '_config.yml', content: config_yml},
91 | {path: '_layouts/base.html', content: layout_base_html},
92 | {path: '_layouts/home.html', content: layout_home_html},
93 | {path: '_layouts/page.html', content: layout_page_html},
94 | {path: 'index.md', content: index_md},
95 | {path: post_path, content: post_md},
96 | {path: '.gitignore', content: gitignore},
97 | ];
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/src/navbar.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class Navbar extends React.Component {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
11 | Toggle navigation
12 |
13 |
14 |
15 |
16 |
JekyllCMS
17 |
18 |
19 |
20 |
21 |
22 | about
23 |
24 |
25 |
26 | {this.props.auth ? : null}
27 |
28 |
29 |
30 |
31 |
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/newfile.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class NewFileModal extends React.Component {
4 | constructor (props) {
5 | super(props);
6 | this.state = {
7 | error: null,
8 | title: "New page",
9 | slug: '',
10 | ext: '.html',
11 | date: moment(),
12 | dirtyForm: true,
13 | };
14 | }
15 | prefix() {
16 | if(this.props.collection.name == 'posts') {
17 | return '_posts/' + this.state.date.format('YYYY-MM-DD') + '-';
18 | }
19 | else {
20 | return '';
21 | }
22 | }
23 | render() {
24 | var collectionSingular = this.props.collection.name.replace(/s$/, '');
25 | var datePicker = null;
26 | if(this.props.collection.name == 'posts') {
27 | datePicker = (
28 |
33 | );
34 | }
35 |
36 | return (
37 |
98 | );
99 | }
100 | componentDidMount() {
101 | this.parseForm();
102 | setTimeout(() => React.findDOMNode(this.refs.title).select(), 500);
103 | }
104 | componentDidUpdate() {
105 | this.parseForm();
106 | }
107 | handleDateChange(date) {
108 | this.setState({date: date, dirtyForm: true});
109 | }
110 | handleTitleChange(e) {
111 | this.setState({title: e.target.value, dirtyForm: true, slug: ''});
112 | }
113 | handleExtChange(e) {
114 | this.setState({ext: e.target.value, dirtyForm: true});
115 | }
116 | handleSlugChange(e) {
117 | this.setState({slug: e.target.value, dirtyForm: true});
118 | }
119 | hasError(slug, path) {
120 | if(slug == '' || slug == '-') {
121 | return "Title is too short";
122 | }
123 |
124 | if(slug.match(/\/$/)) {
125 | return "Slug may not end with '/'";
126 | }
127 |
128 | if(! path.match(/\.(md|markdown|html)$/)) {
129 | return "Invalid file name";
130 | }
131 |
132 | if(this.props.pathExists(path)) {
133 | return "File already exists";
134 | }
135 |
136 | if(! this.props.collection.match({path: path})) {
137 | return "Whoops, we have generated an invalid filename :(";
138 | }
139 | }
140 | parseForm() {
141 | if(! this.state.dirtyForm) { return; }
142 | var slug = this.state.slug || slugify(this.state.title.trim());
143 | var path = generateUnique(
144 | (n) => this.prefix() + slug + n + this.state.ext,
145 | this.props.pathExists
146 | );
147 | this.setState({slug: slug, path: path, dirtyForm: false});
148 |
149 | var error = this.hasError(slug, path);
150 | if(error) {
151 | this.setState({error: error, url: '-'});
152 | }
153 | else {
154 | var fakeFile = this.props.collection.match({path: path}, true);
155 | var url = this.props.config.siteUrl + fakeFile.permalink(true);
156 | this.setState({error: null, url: url});
157 | }
158 | }
159 | handleSubmit(evt) {
160 | evt.preventDefault();
161 | if(this.state.error) { return; }
162 | app.hideModal();
163 | this.props.onCreate({
164 | path: this.state.path,
165 | title: this.state.title.trim(),
166 | });
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/newrepo.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class NewRepo extends React.Component {
4 | render() {
5 | var newRepoHelp = 'https://help.github.com/articles/create-a-repo/';
6 | return (
7 |
8 |
9 |
11 | ×
12 |
13 |
New site
14 |
15 |
16 |
17 | To create a new site, first{' '}
18 |
19 | create a GitHub repository
20 | {' '}
21 | under the {this.props.account.meta.login}
{' '}
22 | {this.props.account.meta.type == 'User' ? 'user' : 'organization'},
23 | then reload this page.
24 |
25 |
26 |
27 | Cancel
32 | Reload
37 |
38 |
39 | );
40 | }
41 | handleReload() {
42 | this.props.onReload();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/newsite.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class NewSiteModal extends React.Component {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
10 | There seems to be no GitHub Pages website in
11 | the {this.props.fullName}
repository. We'd gladly
12 | create one for you, but due to a bug, it's not currently
13 | possible. Please create one by hand.
14 |
15 |
16 |
17 | );
18 | return (
19 |
57 | );
58 | }
59 | componentDidMount() {
60 | setTimeout(() => React.findDOMNode(this.refs.title).select(), 500);
61 | }
62 | handleSubmit(evt) {
63 | evt.preventDefault();
64 | var title = React.findDOMNode(this.refs.title).value.trim();
65 | app.hideModal();
66 | this.props.onCreate({title: title});
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/site.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class Configuration {
4 | constructor(options) {
5 | this.siteUrl = options.siteUrl;
6 | }
7 | permalinkTemplate(collection) {
8 | if(collection == 'posts') {
9 | return '/:categories/:year/:month/:day/:title:output_ext';
10 | }
11 | else {
12 | return '/:path/:basename:output_ext';
13 | }
14 | }
15 | }
16 |
17 | var getConfig = (repo, tree) => {
18 | var getSiteUrl = () => {
19 | var cname = tree.filter((f) => f.path == 'CNAME')[0];
20 | if(cname) {
21 | return cname.getContent()
22 | .then((cnameValue) =>
23 | cnameValue.trim());
24 | }
25 | else {
26 | var name = repo.meta.name;
27 | var ownerLogin = repo.meta.owner.login;
28 | var siteUrl = ownerLogin + '.github.io/' + name;
29 | if(name == ownerLogin + '.github.io' || name == ownerLogin + '.github.com') {
30 | siteUrl = ownerLogin + '.github.io';
31 | }
32 | return Q(siteUrl);
33 | }
34 | };
35 |
36 | var siteUrl;
37 | return getSiteUrl()
38 | .then((value) => {
39 | siteUrl = value;
40 | return new Configuration({
41 | siteUrl: siteUrl,
42 | });
43 | });
44 | };
45 |
46 | class Site extends React.Component {
47 | constructor(props) {
48 | super(props);
49 | this.state = {
50 | tree: null,
51 | config: null,
52 | branch: this.props.repo.branch(this.props.branchName),
53 | };
54 | this.ensureEmailIsVerified();
55 | }
56 | ensureEmailIsVerified() {
57 | var shouldVerifyEmail = () => {
58 | if(this.props.demo) {
59 | return Q(false);
60 | }
61 | else {
62 | return this.props.repo.gh.emailIsVerified()
63 | .then((isVerified) => ! isVerified);
64 | }
65 | }
66 |
67 | shouldVerifyEmail().then((shouldVerify) => {
68 | if(shouldVerify) {
69 | this.warnEmailNotVerified();
70 | }
71 | else {
72 | this.loadInitialFileList();
73 | }
74 | });
75 | }
76 | loadInitialFileList() {
77 | this.props.repo.branches()
78 | .then((branchList) => {
79 | var match = branchList.filter((b) =>
80 | b.name == this.props.branchName);
81 | if(match.length) {
82 | this.updateFileList();
83 | }
84 | else {
85 | this.createNewSite();
86 | }
87 | });
88 | }
89 | render() {
90 | var publicLink = null;
91 | if(this.state.config) {
92 | publicLink = (
93 |
94 |
95 |
96 | );
97 | }
98 |
99 | var siteContents;
100 | if(this.state.tree && this.state.config) {
101 | siteContents = (
102 |
110 | );
111 | }
112 | else {
113 | siteContents = (
114 |
115 | Loading
116 |
117 | );
118 | }
119 |
120 | return (
121 |
122 |
123 | {this.props.repo.meta.full_name}{' '}
124 | {publicLink}
125 |
126 | {siteContents}
127 |
128 | );
129 | }
130 | createFile(path) {
131 | return this.state.branch.newFile(path);
132 | }
133 | updateFileList() {
134 | this.state.branch.files()
135 | .then((tree) => {
136 | this.setState({tree: tree});
137 | getConfig(this.props.repo, tree)
138 | .then((config) => {
139 | this.setState({config: config});
140 | });
141 | })
142 | .catch(errorHandler("loading file list"));
143 | }
144 | createNewSite() {
145 | var handleSiteCreate = (options) => {
146 | options.repo = this.props.repo.meta.name;
147 | var tree = initialContent(options);
148 | this.props.repo.createBranch(this.props.branchName, tree)
149 | .then(() => {
150 | this.updateFileList();
151 | });
152 | };
153 |
154 | app.modal(
155 |
160 | );
161 | }
162 | warnEmailNotVerified() {
163 | app.modal(
164 |
167 | );
168 | }
169 | }
170 |
171 | class SiteWrapper extends React.Component {
172 | constructor(props) {
173 | super(props);
174 | this.state = {};
175 | }
176 | componentDidMount() {
177 | let {query} = this.props.location;
178 | let {userName, repoName} = this.props.params;
179 | let gitHub = GitHub.create(query.demo);
180 | return gitHub.repo(userName + '/' + repoName)
181 | .then((repo) => {
182 | this.setState({
183 | repo: repo,
184 | branchName: query['branch'] ? query['branch'] : repo.getDefaultBranchName()
185 | });
186 | })
187 | .catch(errorHandler("loading repository"));
188 | }
189 | render() {
190 | let {query} = this.props.location;
191 | let {userName} = this.props.params;
192 | let {branchName, repo} = this.state;
193 | if (!branchName) {
194 | return false
195 | }
196 | return ;
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | .authbox {
2 | text-align: center;
3 | }
4 |
5 | .accountList {
6 | padding: 0;
7 | }
8 |
9 | .accountList > li {
10 | list-style: none;
11 | border-bottom: 1px solid #eee;
12 | padding: 10px 0;
13 | }
14 |
15 | .accountList > li > a.buttonlink {
16 | display: block;
17 | overflow: hidden;
18 | }
19 |
20 | .accountList > li > a.buttonlink > img {
21 | display: block;
22 | float: left;
23 | width: 64px; height: 64px;
24 | margin: 0 15px;
25 | }
26 |
27 | .accountList > li > a.buttonlink > p {
28 | float: left;
29 | font-size: 150%;
30 | color: #555;
31 | }
32 |
33 | .accountList > li > a.buttonlink > .accountSelectedMarker {
34 | float: right;
35 | font-size: 300%;
36 | color: #555;
37 | }
38 |
39 | .accountRepoList > li {
40 | border-bottom: 1px solid #eee;
41 | list-style: none;
42 | padding: 10px 0;
43 | }
44 |
45 | .accountRepoList > li > a.buttonlink {
46 | display: block;
47 | overflow: hidden;
48 | text-decoration: none;
49 | }
50 |
51 | .accountRepoList > li > a.buttonlink > p {
52 | color: #888;
53 | }
54 |
55 | .fileList {
56 | padding: 0;
57 | }
58 |
59 | .file {
60 | list-style: none;
61 | }
62 |
63 | .file > a {
64 | display: block;
65 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
66 | }
67 |
68 | .file.current {
69 | background: #d9edf7;
70 | }
71 |
72 | .file.new {
73 | font-style: italic;
74 | }
75 |
76 | .contentEditor {
77 | margin: 10px 0;
78 | position: relative;
79 | }
80 |
81 | .aceContainer {
82 | height: 200px;
83 | }
84 |
85 | a.buttonlink {
86 | cursor: pointer;
87 | }
88 |
89 | .site {
90 | position: relative;
91 | }
92 |
93 | .editor-container {
94 | position: absolute; top: 0; left: 0; right: 0; height: 0;
95 | }
96 |
97 | .editor {
98 | background: white;
99 | box-shadow: -4px 1px 3px rgba(0, 0, 0, 0.2);
100 | }
101 |
102 | .inline-fa {
103 | font-size: .6em;
104 | }
105 |
106 | #errors {
107 | position: absolute;
108 | top: 5px;
109 | right: 5px;
110 | z-index: 10;
111 | }
112 |
113 | .loading {
114 | font-size: 24px;
115 | color: #888;
116 | text-align: center;
117 | padding-top: 50px;
118 | }
119 |
120 | .newFile code {
121 | color: #4a5693;
122 | background: #f1f6ff;
123 | }
124 |
125 | .newFile .slug {
126 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
127 | padding: 0;
128 | background: none;
129 | border: none;
130 | border-bottom: 1px dashed #456cb5;
131 | }
132 |
133 | .editorHelp {
134 | text-align: right;
135 | color: #888;
136 | }
137 |
138 | .ckMediaBrowser img {
139 | max-width: 300px;
140 | max-height: 300px;
141 | border: 1px solid #aaa;
142 | }
143 |
--------------------------------------------------------------------------------
/src/track.jsx:
--------------------------------------------------------------------------------
1 | function trackPiwik(options) {
2 | var _paq = window._paq = window._paq || [];
3 | _paq.push(['trackPageView']);
4 | _paq.push(['enableLinkTracking']);
5 | var u=options.piwikUrl;
6 | _paq.push(['setTrackerUrl', u+'piwik.php']);
7 | _paq.push(['setSiteId', options.siteId]);
8 | var d=document, g=d.createElement('script'),
9 | s=d.getElementsByTagName('script')[0];
10 | g.type='text/javascript'; g.async=true; g.defer=true;
11 | g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
12 | }
13 |
--------------------------------------------------------------------------------
/src/verify.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class EmailNotVerified extends React.Component {
4 | render() {
5 | var url = 'https://help.github.com/articles/verifying-your-email-address/';
6 | return (
7 |
8 |
9 |
11 | ×
12 |
13 |
Verify email
14 |
15 |
16 |
17 | Your email address is not verified by GitHub.
18 | Please verify it .
19 |
20 |
21 |
22 |
24 | Cancel
25 |
26 |
28 | Try again
29 |
30 |
31 |
32 | );
33 | }
34 | handleRetry(evt) {
35 | evt.preventDefault();
36 | app.hideModal();
37 | this.props.onRetry();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------