├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── app ├── controllers │ └── index.js └── views │ └── basic.html ├── config ├── config.js ├── koa.js └── routes.js ├── images ├── alipay_qr.jpg ├── eevee.jpg └── weixin-qr.jpg ├── package.json ├── register-babel.js ├── server.js ├── src ├── actions │ └── LeafActions.js ├── common │ └── lib.js ├── components │ ├── Desktop │ │ ├── AddFile.jsx │ │ ├── Aside.jsx │ │ ├── Head.jsx │ │ └── List.jsx │ ├── Login │ │ └── loginForm.jsx │ └── Post │ │ ├── Editor.jsx │ │ ├── Head.jsx │ │ └── Meta.jsx ├── constants │ └── LeafActionTypes.js ├── containers │ ├── Desktop.jsx │ ├── Dir.jsx │ ├── Login.jsx │ └── Post.jsx ├── entry │ └── App.jsx ├── middleware │ ├── index.js │ └── promiseMiddleware.js ├── reducers │ ├── Blob.js │ ├── RepoInfo.js │ ├── RepoTree.js │ ├── Tree.js │ ├── User.js │ ├── auth.js │ └── index.js ├── services │ ├── auth.js │ ├── repo.js │ └── user.js ├── stores │ └── index.js ├── styles │ ├── CodeMirror.less │ ├── editIcon.less │ └── leaf.less └── utils │ ├── editorFormat.js │ └── localStorage.js ├── test ├── .eslintrc └── test.storage.js ├── webpack.config.js └── webpack ├── strategies ├── development.js ├── index.js ├── optimize.js ├── style.js └── version.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "strict": 0, 10 | "valid-jsdoc": 2, 11 | "react/jsx-uses-react": 2, 12 | "react/jsx-uses-vars": 2, 13 | "react/react-in-jsx-scope": 2, 14 | 15 | // Disable until Flow supports let and const 16 | "no-var": 0, 17 | "vars-on-top": 0, 18 | 19 | // Disable comma-dangle unless need to support it 20 | "comma-dangle": 0, 21 | "consistent-return": 1 22 | }, 23 | "plugins": [ 24 | "react" 25 | ], 26 | "parser": "babel-eslint" 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | .ipr 4 | .iws 5 | *~ 6 | ~* 7 | *.diff 8 | *.patch 9 | *.bak 10 | .DS_Store 11 | Thumbs.db 12 | .project 13 | .*proj 14 | .svn/ 15 | *.swp 16 | *.swo 17 | *.pyc 18 | *.pyo 19 | node_modules 20 | dist 21 | build/ 22 | public/ 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | # Eevee - 伊布 [![Build Status](https://travis-ci.org/pizn/eevee.svg?branch=master)](https://travis-ci.org/pizn/eevee) 8 | 9 |

基于 Github Pages 的在线编辑平台,让你更加专注于内容的编写.

10 | 11 | 12 | 13 | ### 初衷 14 | 15 | 像黑客一样写博客太麻烦了 -- 需要在你的编辑器(Vim/Mou...)打开项目,然后编辑一篇 markdown 文章,写完之后还要 add/commit/push 等动作.为何不能简单些呢?我只关注于内容不是更好么?于是就有了这样的一个想法,需要有一个工具,让我可以在任何地方,任何时候,想写就写.这个工具,它就是 `伊布`.为喜欢在 GitHub 上分享文章的人精心打造. 16 | 17 | ### 构成及原理 18 | 19 | 纯前端实现,可以说没有 Server 层.通过 GitHub API 与你在 GitHub 上的代码库取得联系,获取 Project 的文章(_posts/),完成增删查改的功能.依赖的数据前提: 20 | 21 | * GitHub 账号(只在浏览器中记录) 22 | * 基于 Jekyll 创建好的 Pages 项目, 文章存放在 `_posts` 目录下 23 | 24 | ### 演示及 Jekyll 主题结合 25 | 26 | * 直接访问 [Eevee Online](http://pizn.github.io/eevee), 用 GitHub 账号登录, 可以编辑你的 `username.github.io` 上 `_posts` 目录下的文章 27 | * Fork 我的 Jekyll 主题 -- [leafeon](http://github.com/pizn/leafeon) 到你的 `username.github.io` 上, 即可完成编辑 28 | 29 | ### 如何使用 30 | 31 | 1. 使用 GitHub 账号登录 Eevee(前提是你已经基于 GitHub Pages 建立好博客) 32 | 2. 选择文件,编辑, `Command + s` 保存即可 33 | 3. 稍等片刻,你的博客则刷新出新的文章 34 | 35 | ### 参与开发 36 | 37 | 该项目基于 React + Ant Design + GitHub API 完成. 38 | 39 | 1. npm install 40 | 2. npm run hot-dev-server 41 | 3. npm run dev 42 | 43 | ### 特性 44 | 45 | - [x] 登录 GitHub 账号,获取 `*.github.io` 或者 `*.github.com` 的 Project 46 | - [x] 获取 `_posts` 的所有文档(仅 markdown ) 47 | - [x] 添加文章 48 | - [x] 编辑文章 49 | - [x] 删除文章 50 | 51 | ### 计划 52 | 53 | - [ ] 可自动创建 Project 54 | - [ ] 提供草稿编辑功能 55 | - [ ] 管理图片等静态文件功能 56 | - [ ] 编辑配置 57 | 58 | ## Author 59 | 60 | #### PIZn 61 | 62 | * https://github.com/pizn 63 | * https://twitter.com/piznlin 64 | * http://www.pizn.net 65 | 66 | #### Donate 67 | 68 | 如果你认为我做的这些对你来说是有价值的, 并鼓励我进行更多开源和免费的开发. 那你可以资助我, 就算是一杯咖啡...If you find my work useful and you want to encourage the development of more free resources, you can do it by donating. 69 | 70 | PIZn 的支付宝二维码: 71 | 72 | 73 | 74 | PIZn 的微信支付二维码: 75 | 76 | 77 | 78 | ## License 79 | 80 | Copyright (c) 2016 PIZn. 81 | 82 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 83 | 84 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 85 | 86 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 87 | -------------------------------------------------------------------------------- /app/controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const pkg = require('../../package.json'); 3 | const stats = require("../../build/stats.json"); 4 | const publicPath = stats.publicPath; 5 | var STYLE_URL; 6 | var SCRIPT_URL_APP = publicPath + [].concat(stats.assetsByChunkName.app)[0]; 7 | if (process.env.NODE_ENV === "production") { 8 | STYLE_URL = '/' + pkg.version + '/' + (publicPath + [].concat(stats.assetsByChunkName.app)[1] + "?" + stats.hash); 9 | SCRIPT_URL_APP = '/' + pkg.version + '/' + SCRIPT_URL_APP + "?" + stats.hash; 10 | } 11 | 12 | 13 | exports.index = function *() { 14 | this.body = yield this.render("basic", { 15 | title: "Eevee", 16 | STYLE_URL: STYLE_URL, 17 | SCRIPT_URL: SCRIPT_URL_APP, 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /app/views/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{title}} 7 | {% if STYLE_URL %} 8 | 9 | {% endif %} 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require("path"); 4 | const _ = require("lodash"); 5 | 6 | const env = process.env.NODE_ENV = process.env.NODE_ENV || "development"; 7 | 8 | const base = { 9 | app: { 10 | root: path.normalize(path.join(__dirname, "/..")), 11 | env: env, 12 | }, 13 | }; 14 | 15 | const specific = { 16 | development: { 17 | app: { 18 | port: 3000, 19 | name: "Lark chat - Dev", 20 | keys: [ "super-secret-hurr-durr" ], 21 | }, 22 | }, 23 | test: { 24 | app: { 25 | port: 3000, 26 | name: "Lark chat - Test", 27 | keys: [ "super-secret-hurr-durr" ], 28 | }, 29 | }, 30 | production: { 31 | app: { 32 | port: process.env.PORT || 3000, 33 | name: "Lark chat - Test", 34 | }, 35 | } 36 | }; 37 | 38 | module.exports = _.merge(base, specific[env]); -------------------------------------------------------------------------------- /config/koa.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require("path"); 4 | const serve = require("koa-static-cache"); 5 | const session = require("koa-generic-session"); 6 | const logger = require("koa-logger"); 7 | const errorHandler = require("koa-error"); 8 | const bodyParser = require("koa-bodyparser"); 9 | const compress = require("koa-compress"); 10 | const views = require("co-views"); 11 | const responseTime = require("koa-response-time"); 12 | 13 | const STATIC_FILES_MAP = {}; 14 | const SERVE_OPTIONS = { maxAge: 365 * 24 * 60 * 60 }; 15 | 16 | module.exports = function(app, config) { 17 | 18 | if (config.app.env !== "test") { 19 | app.use(logger()); 20 | } 21 | 22 | app.use(errorHandler()); 23 | 24 | if (config.app.env === "production") { 25 | app.use(serve(path.join(config.app.root, "public"), SERVE_OPTIONS, STATIC_FILES_MAP)); 26 | } else { 27 | app.use(require("koa-proxy")({ 28 | host: "http://localhost:2992", 29 | match: /^\/_assets\//, 30 | })); 31 | } 32 | 33 | app.use(bodyParser()); 34 | 35 | app.use(function *(next) { 36 | this.render = views(config.app.root + "/app/views", { 37 | map: { html: "swig" }, 38 | cache: config.app.env === "development" ? "memory" : false, 39 | }); 40 | yield next; 41 | }); 42 | 43 | app.use(compress()); 44 | app.use(responseTime()); 45 | } -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Router = require("koa-router"); 4 | 5 | const indexController = require("../app/controllers/index"); 6 | 7 | module.exports = function(app) { 8 | const router = new Router(); 9 | 10 | router.use(function *(next) { 11 | this.type = "json"; 12 | yield next; 13 | }); 14 | 15 | router.get("/", function *() { 16 | this.type = "html"; 17 | yield indexController.index.apply(this); 18 | }); 19 | 20 | app.use(router.routes()); 21 | }; 22 | -------------------------------------------------------------------------------- /images/alipay_qr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pizn/eevee/59f3556bcbfa6b275e6bd97f73b3c52de89ae5be/images/alipay_qr.jpg -------------------------------------------------------------------------------- /images/eevee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pizn/eevee/59f3556bcbfa6b275e6bd97f73b3c52de89ae5be/images/eevee.jpg -------------------------------------------------------------------------------- /images/weixin-qr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pizn/eevee/59f3556bcbfa6b275e6bd97f73b3c52de89ae5be/images/weixin-qr.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eevee", 3 | "version": "0.0.2", 4 | "description": "伊布 - 内容为王,基于 GitHub 的博客管理平台", 5 | "keywords": [ 6 | "jekyll", 7 | "github" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/pizn/eevee.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/pizn/eevee.git/issues" 15 | }, 16 | "license": "MIT", 17 | "entry": { 18 | "index": "./src/entry/App.jsx" 19 | }, 20 | "dependencies": { 21 | "antd": "0.11.x", 22 | "babel": "~5.8.29", 23 | "babel-cli": "^6.1.18", 24 | "babel-core": "~5.8.33", 25 | "babel-eslint": "~4.1.6", 26 | "babel-loader": "~5.3.2", 27 | "babel-plugin-antd": "~0.2.0", 28 | "babel-plugin-react-intl": "^2.0.0", 29 | "babel-plugin-react-transform": "~1.1.1", 30 | "babel-plugin-transform-object-rest-spread": "^6.1.18", 31 | "babel-preset-es2015": "^6.1.18", 32 | "babel-preset-es2015-rollup": "^1.0.0", 33 | "babel-preset-react": "^6.1.18", 34 | "babel-register": "^6.2.0", 35 | "babel-runtime": "~5.8.20", 36 | "babelify": "^7.2.0less", 37 | "base-64": "~0.1.0", 38 | "classnames": "~2.2.3", 39 | "co": "~4.6.0", 40 | "co-views": "~1.0.0", 41 | "codemirror": "~5.9.0", 42 | "css-loader": "^0.23.1", 43 | "detector": "2.4.1", 44 | "extract-text-webpack-plugin": "~1.0.1", 45 | "github-api": "~0.11.2", 46 | "highlight.js": "~9.1.0", 47 | "history": "~1.17.0", 48 | "js-yaml": "~3.5.2", 49 | "keymaster": "~1.6.2", 50 | "koa": "~1.0.0", 51 | "koa-bodyparser": "~2.0.1", 52 | "koa-compress": "1.0.x", 53 | "koa-error": "1.1.x", 54 | "koa-generic-session": "~1.9.2", 55 | "koa-logger": "1.3.0", 56 | "koa-passport": "~1.1.5", 57 | "koa-proxy": "^0.3.0", 58 | "koa-response-time": "1.0.x", 59 | "koa-router": "~5.2.1", 60 | "koa-static-cache": "~3.1.2", 61 | "less": "~2.5.0", 62 | "less-loader": "~2.2.0", 63 | "lodash": "~3.10.1", 64 | "marked": "~0.3.5", 65 | "mocha": "~2.3.4", 66 | "moment": "~2.10.6", 67 | "nodemon": "~1.4.1", 68 | "null-loader": "~0.1.1", 69 | "object-assign": "~2.0.0", 70 | "rc-form": "~0.7.3", 71 | "react": "~0.14.3", 72 | "react-dom": "~0.14.3", 73 | "react-hot-loader": "~1.2.9", 74 | "react-proxy-loader": "~0.3.4", 75 | "react-redux": "~4.0.6", 76 | "react-router": "~1.0.3", 77 | "redux": "~3.1.3", 78 | "redux-devtools": "~3.0.1", 79 | "redux-promise": "~0.5.0", 80 | "redux-thunk": "~1.0.3", 81 | "style-loader": "~0.13.0", 82 | "swig": "~1.4.2", 83 | "utf8": "~2.1.1", 84 | "webpack": "^1.12.9", 85 | "webpack-dev-middleware": "^1.4.0", 86 | "webpack-dev-server": "~1.10.1", 87 | "webpack-hot-middleware": "~2.5.1", 88 | "webpack-isomorphic-tools": "^0.8.8", 89 | "yargs": "~3.21.0" 90 | }, 91 | "devDependencies": { 92 | "babel-plugin-transform-decorators-legacy": "~1.3.4", 93 | "eslint": "~1.10.3", 94 | "eslint-config-airbnb": "~2.1.0", 95 | "eslint-plugin-babel": "~3.0.0", 96 | "eslint-plugin-react": "~3.11.3", 97 | "pre-commit": "1.x" 98 | }, 99 | "pre-commit": [ 100 | "lint" 101 | ], 102 | "scripts": { 103 | "dev": "NODE_ENV=development ./node_modules/.bin/nodemon --harmony server.js", 104 | "prod": "npm run build && NODE_ENV=production node --harmony server.js", 105 | "build": "NODE_ENV=production webpack --config webpack.config.js --separate-stylesheet -p --progress --profile --colors", 106 | "hot-dev-server": "webpack-dev-server --content-base public -ds --config webpack.config.js --hot --progress --colors --inline", 107 | "dev-server": "npm run hot-dev-server", 108 | "lint": "eslint --ext .js,.jsx src", 109 | "test": "npm run base-test && npm run lint", 110 | "base-test": "NODE_ENV=test ./node_modules/.bin/mocha --harmony --reporter spec ./test/test-*.js", 111 | "validate": "npm ls" 112 | }, 113 | "theme": { 114 | "primary-color": "#343434", 115 | "link-color": "#343434", 116 | "border-color-base": "#e2e7ec", 117 | "btn-border-radius-base": "32px" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /register-babel.js: -------------------------------------------------------------------------------- 1 | require("babel/register")({ 2 | ignore: /node_modules/, 3 | optional: ["es7.objectRestSpread", "runtime"] 4 | }); 5 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const koa = require("koa"); 3 | const http = require('http'); 4 | 5 | /** 6 | * Config 7 | */ 8 | const config = require("./config/config"); 9 | 10 | /** 11 | * Server 12 | */ 13 | const app = module.exports = koa(); 14 | 15 | // Routes 16 | require("./config/koa")(app, config); 17 | require("./config/routes")(app); 18 | 19 | // Start app 20 | if (!module.parent) { 21 | app.listen(config.app.port); 22 | console.log("Server started, listening on port: " + config.app.port); 23 | } 24 | console.log("Environment: " + config.app.env); 25 | -------------------------------------------------------------------------------- /src/actions/LeafActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/LeafActionTypes'; 2 | import auth from '../services/auth'; 3 | import user from '../services/user'; 4 | import repo from '../services/repo'; 5 | 6 | export function login(data) { 7 | return { 8 | types: [types.AUTH_LOGIN, types.AUTH_LOGIN_SUCCESS, types.AUTH_LOGIN_FAIL], 9 | promise: auth.login(data), 10 | data, 11 | }; 12 | } 13 | 14 | export function loginDone() { 15 | // login Done 16 | auth.loginDone(); 17 | return { 18 | type: types.AUTH_LOGIN_DONE, 19 | }; 20 | } 21 | 22 | export function logout() { 23 | return { 24 | types: [types.AUTH_LOGOUT, types.AUTH_LOGOUT_SUCCESS, types.AUTH_LOGOUT_FAIL], 25 | promise: auth.logout(), 26 | }; 27 | } 28 | 29 | export function updateUserInfo(data) { 30 | if (data) { 31 | return { 32 | type: types.UPDATE_USER_INFO, 33 | data, 34 | }; 35 | } 36 | return { 37 | types: [types.LOAD_USER_INFO, types.LOAD_USER_INFO_SUCCESS, types.LOAD_USER_INFO_FAIL], 38 | promise: user.getInfo(), 39 | }; 40 | } 41 | 42 | export function loadRepoInfo(data) { 43 | return { 44 | types: [types.LOAD_REPO_INFO, types.LOAD_REPO_INFO_SUCCESS, types.LOAD_REPO_INFO_FAIL], 45 | promise: user.checkRepo(data.username), 46 | }; 47 | } 48 | 49 | export function updateRepoInfo(data) { 50 | return { 51 | type: types.UPLOAD_REPO_INFO, 52 | data, 53 | }; 54 | } 55 | 56 | export function loadRepoTree(data) { 57 | return { 58 | types: [types.LOAD_REPO_TREE, types.LOAD_REPO_TREE_SUCCESS, types.LOAD_REPO_TREE_FAIL], 59 | promise: repo.getTree(data.username, data.reponame), 60 | }; 61 | } 62 | 63 | export function readRepoTree(data) { 64 | return { 65 | types: [types.READ_REPO_TREE, types.READ_REPO_TREE_SUCCESS, types.READ_REPO_TREE_FAIL], 66 | promise: repo.readTree(data.username, data.reponame, data.path), 67 | }; 68 | } 69 | 70 | export function readRepoBlob(data) { 71 | return { 72 | types: [types.READ_REPO_BLOB, types.READ_REPO_BLOB_SUCCESS, types.READ_REPO_BLOB_FAIL], 73 | promise: repo.readBlob(data.username, data.reponame, data.path), 74 | }; 75 | } 76 | 77 | export function readRepoBlobCommit(data) { 78 | return { 79 | types: [types.READ_REPO_BLOB_COMMIT, types.READ_REPO_BLOB_COMMIT_SUCCESS, types.READ_REPO_BLOB_COMMIT_FAIL], 80 | promise: repo.readBlobCommit(data.username, data.reponame, data.sha), 81 | }; 82 | } 83 | 84 | export function addRepoBlob(data) { 85 | return { 86 | types: [types.ADD_REPO_BLOB, types.ADD_REPO_BLOB_SUCCESS, types.ADD_REPO_BLOB_FAIL], 87 | promise: repo.addBlob(data), 88 | }; 89 | } 90 | 91 | export function updateRepoBlob(data) { 92 | return { 93 | types: [types.UPDATE_REPO_BLOB, types.UPDATE_REPO_BLOB_SUCCESS, types.UPDATE_REPO_BLOB_FAIL], 94 | promise: repo.writeBlob(data), 95 | data, 96 | }; 97 | } 98 | 99 | export function removeRepoBlob(data) { 100 | return { 101 | types: [types.REMOVE_REPO_BLOB, types.REMOVE_REPO_BLOB_SUCCESS, types.REMOVE_REPO_BLOB_FAIL], 102 | promise: repo.removeBlob(data), 103 | data, 104 | }; 105 | } 106 | 107 | export function clearRepoBlob() { 108 | return { 109 | type: types.CLEAR_REPO_BLOB, 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /src/common/lib.js: -------------------------------------------------------------------------------- 1 | import 'antd/style/index.less'; 2 | import '../styles/leaf.less'; 3 | -------------------------------------------------------------------------------- /src/components/Desktop/AddFile.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Form, Item as FormItem } from 'antd/lib/form'; 3 | 4 | import Modal from 'antd/lib/modal'; 5 | import Col from 'antd/lib/col'; 6 | import Input from 'antd/lib/input'; 7 | import Button from 'antd/lib/button'; 8 | 9 | import createForm from 'rc-form/lib/createForm'; 10 | 11 | 12 | class CreateFileForm extends Component { 13 | 14 | static propTypes = { 15 | modalVisible: PropTypes.bool, 16 | modalHandleOk: PropTypes.func, 17 | modalHandleCancel: PropTypes.func, 18 | form: PropTypes.object, 19 | } 20 | 21 | modalHandleOk(e) { 22 | e.preventDefault(); 23 | const { form, modalHandleOk } = this.props; 24 | const { validateFields } = form; 25 | validateFields((error, values) => { 26 | if (!error) { 27 | modalHandleOk(values); 28 | } else { 29 | console.log('error', error, values); 30 | } 31 | }); 32 | } 33 | 34 | render() { 35 | const { modalVisible, modalHandleOk, modalHandleCancel, form } = this.props; 36 | const { getFieldProps, getFieldError } = form; 37 | return ( 38 | Cancle, 45 | 48 | ]} 49 | > 50 |
51 | 57 | 68 | 69 |

{getFieldError('name') ? getFieldError('name').join('') : ''}

70 | 71 |
72 | 78 | 88 | 89 |

{getFieldError('title') ? getFieldError('title').join('') : ''}

90 | 91 |
92 | 98 | 108 | 109 |

{getFieldError('description') ? getFieldError('description').join('') : ''}

110 | 111 |
112 |
113 |
114 | ); 115 | } 116 | } 117 | 118 | export default createForm()(CreateFileForm); 119 | -------------------------------------------------------------------------------- /src/components/Desktop/Aside.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Row from 'antd/lib/row'; 4 | import Col from 'antd/lib/col'; 5 | import Icon from 'antd/lib/icon'; 6 | 7 | import { Link } from 'react-router'; 8 | import classNames from 'classnames'; 9 | 10 | class Aside extends Component { 11 | 12 | static propTypes = { 13 | logout: PropTypes.func, 14 | user: PropTypes.object, 15 | repoInfo: PropTypes.object, 16 | tree: PropTypes.object, 17 | } 18 | 19 | logout() { 20 | const { logout } = this.props; 21 | logout(); 22 | } 23 | render() { 24 | const { user, repoInfo, tree } = this.props; 25 | 26 | const logoCls = classNames({ 27 | 'head-logo': true, 28 | 'head-logo-loading': user.loading | repoInfo.loading | tree.loading, 29 | }); 30 | 31 | const countCls = classNames({ 32 | 'count': true, 33 | 'count-active': tree.loaded, 34 | }); 35 | 36 | return ( 37 |
38 |
39 |
40 |
41 |
42 | 71 |
72 | 73 |
74 | { !user.loading && 75 | 76 | 77 | 78 | 79 | 80 |

{user.data.name || user.data.login}

81 |

{user.data.email}

82 | 83 | 88 |
89 | } 90 | 91 |

Build with Love in Eevee.

92 |
93 |
94 |
95 | ); 96 | } 97 | } 98 | 99 | export default Aside; 100 | -------------------------------------------------------------------------------- /src/components/Desktop/Head.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Row from 'antd/lib/row'; 4 | import Col from 'antd/lib/col'; 5 | import Icon from 'antd/lib/icon'; 6 | import Button from 'antd/lib/button'; 7 | 8 | import { Link } from 'react-router'; 9 | 10 | class Head extends Component { 11 | 12 | static propTypes = { 13 | logout: PropTypes.func, 14 | addFile: PropTypes.func, 15 | repoInfo: PropTypes.object, 16 | params: PropTypes.object, 17 | } 18 | 19 | render() { 20 | const { addFile, repoInfo, params } = this.props; 21 | let crumb; 22 | if (params && params.splat) { 23 | const crumbData = params.splat.split('/'); 24 | const child = crumbData.map((item, index) => { 25 | const data = { 26 | name: item, 27 | path: crumbData.slice(0, index + 1).join('/'), 28 | }; 29 | return ( 30 | 31 | { index === crumbData.length - 1 && 32 | {item} 33 | } 34 | { index !== crumbData.length - 1 && 35 | 36 | {data.name} 37 | 38 | } 39 | 40 | ); 41 | }); 42 | 43 | crumb = ( 44 | 45 | _posts 46 | {child} 47 | 48 | ); 49 | } else { 50 | crumb = `_posts`; 51 | } 52 | 53 | return ( 54 |
55 | { repoInfo.loaded && 56 | 57 | 58 |

59 | {repoInfo.data.name} 60 | 61 | {crumb} 62 |

63 | 64 | 65 | 66 | 67 |
68 | } 69 |
70 | ); 71 | } 72 | } 73 | 74 | export default Head; 75 | -------------------------------------------------------------------------------- /src/components/Desktop/List.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Icon from 'antd/lib/icon'; 3 | 4 | import classNames from 'classnames'; 5 | import { Link } from 'react-router'; 6 | 7 | class List extends Component { 8 | 9 | static propTypes = { 10 | tree: PropTypes.object, 11 | } 12 | 13 | render() { 14 | const { tree } = this.props; 15 | let files; 16 | if (tree.loaded) { 17 | files = ( 18 | tree.data.map(item => { 19 | let fileName = item.name.split(/^((?:19|20)\d\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])-/); 20 | fileName = fileName[4] || item.name; 21 | const filePath = item.path.split('_posts/')[1]; 22 | const fileCls = classNames({ 23 | 'file': true, 24 | 'file-f': item.type === 'file', 25 | 'file-d': item.type === 'dir' 26 | }); 27 | 28 | return ( 29 |
  • 30 | { item.type === 'file' && 31 | 32 |
    33 |
    34 | {fileName} 35 |
    36 | 37 | } 38 | { item.type === 'dir' && 39 | 40 |
    41 |
    42 | {item.name} 43 |
    44 | 45 | } 46 |
  • 47 | ); 48 | }) 49 | ); 50 | } 51 | return ( 52 |
    53 | { !tree.loaded && 54 |
    55 | { !tree.error && 56 | 57 | } 58 | { tree.error && 59 | 60 | } 61 |
    62 | } 63 | { tree.loaded && 64 | 67 | } 68 |
    69 | ); 70 | } 71 | } 72 | 73 | export default List; 74 | -------------------------------------------------------------------------------- /src/components/Login/loginForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Row from 'antd/lib/row'; 4 | import Col from 'antd/lib/col'; 5 | import Input from 'antd/lib/input'; 6 | import Button from 'antd/lib/button'; 7 | import Alert from 'antd/lib/alert'; 8 | import Icon from 'antd/lib/icon'; 9 | 10 | import { Form, Item as FormItem } from 'antd/lib/form'; 11 | 12 | import createForm from 'rc-form/lib/createForm'; 13 | 14 | class loginForm extends Component { 15 | 16 | static propTypes = { 17 | onSubmit: PropTypes.func, 18 | form: PropTypes.object, 19 | auth: PropTypes.object, 20 | } 21 | 22 | onSubmit(e) { 23 | e.preventDefault(); 24 | const { onSubmit, form } = this.props; 25 | const { validateFields } = form; 26 | validateFields((error, values) => { 27 | if (!error) { 28 | onSubmit(values); 29 | } 30 | }); 31 | } 32 | 33 | render() { 34 | const { form, auth } = this.props; 35 | const { getFieldProps, getFieldError } = form; 36 | return ( 37 |
    38 | {auth.error && 39 | 40 | } 41 | 48 | ()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 53 | message: 'Please input your GitHub Email', 54 | }] })} 55 | /> 56 | 57 |

    { getFieldError('email') ? getFieldError('email') + '' : '' }

    58 | 59 |
    60 | 67 | 73 | 74 |

    { getFieldError('pass') ? getFieldError('pass') + '' : '' }

    75 | 76 |
    77 | 78 | 79 | 82 | 83 | 84 | 85 | ); 86 | } 87 | } 88 | 89 | export default createForm()(loginForm); 90 | -------------------------------------------------------------------------------- /src/components/Post/Editor.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | import CM from 'codemirror/lib/codemirror'; 4 | import { getCursorState, applyFormat } from '../../utils/editorFormat'; 5 | 6 | import marked from 'marked/lib/marked'; 7 | // import hljs from 'highlight.js/lib/highlight.js'; 8 | import key from 'keymaster'; 9 | import Icon from 'antd/lib/icon'; 10 | import Tooltip from 'antd/lib/tooltip'; 11 | import '../../styles/EditIcon.less'; 12 | import '../../styles/CodeMirror.less'; 13 | 14 | marked.setOptions({ 15 | renderer: new marked.Renderer() 16 | }); 17 | 18 | require('codemirror/mode/xml/xml'); 19 | require('codemirror/mode/markdown/markdown'); 20 | require('codemirror/mode/gfm/gfm'); 21 | require('codemirror/mode/javascript/javascript'); 22 | require('codemirror/mode/css/css'); 23 | require('codemirror/mode/htmlmixed/htmlmixed'); 24 | require('codemirror/mode/dockerfile/dockerfile'); 25 | require('codemirror/mode/go/go'); 26 | require('codemirror/mode/nginx/nginx'); 27 | require('codemirror/mode/shell/shell'); 28 | require('codemirror/mode/sql/sql'); 29 | require('codemirror/mode/swift/swift'); 30 | require('codemirror/mode/velocity/velocity'); 31 | require('codemirror/mode/yaml/yaml'); 32 | require('codemirror/mode/http/http'); 33 | require('codemirror/mode/clike/clike'); 34 | require('codemirror/addon/edit/continuelist'); 35 | 36 | class Editor extends Component { 37 | 38 | static propTypes = { 39 | onChange: PropTypes.func, 40 | options: PropTypes.object, 41 | path: PropTypes.string, 42 | value: PropTypes.string, 43 | history: PropTypes.object, 44 | blob: PropTypes.object, 45 | params: PropTypes.object, 46 | handleSave: PropTypes.func, 47 | className: PropTypes.string, 48 | } 49 | 50 | constructor(props, context) { 51 | super(props, context); 52 | this.state = { 53 | isPreview: false, 54 | onScrolled: false, 55 | cs: {}, 56 | }; 57 | } 58 | 59 | componentDidMount() { 60 | const { value } = this.props; 61 | const editorNode = this.refs.editor; 62 | const that = this; 63 | this.codeMirror = CM.fromTextArea(editorNode, this.getOptions()); 64 | this.codeMirror.on('change', this.codemirrorValueChanged.bind(this)); 65 | this.codeMirror.on('cursorActivity', this.updateCursorState.bind(this)); 66 | this._currentCodemirrorValue = value; 67 | 68 | // 保存 69 | key('⌘+s, ctrl+s', (e) => { 70 | e.preventDefault(); 71 | that.handleSave(); 72 | }); 73 | 74 | // 返回 75 | key('⌘+shift+left', (e) => { 76 | e.preventDefault(); 77 | that.handleBack(); 78 | }); 79 | 80 | // 预览 81 | key('⌘+0, ctrl+0', (e) => { 82 | e.preventDefault(); 83 | that.handlePreviewEvent(); 84 | }); 85 | } 86 | 87 | componentWillReceiveProps(nextProps) { 88 | if (this.codeMirror && nextProps.value !== undefined && this._currentCodemirrorValue !== nextProps.value) { 89 | this.codeMirror.setValue(nextProps.value); 90 | this.codeMirror.refresh(); 91 | this.codeMirror.focus(); 92 | } 93 | } 94 | 95 | /* 96 | * 1. 切换视图 97 | * 2. 滚动条位置 98 | * 3. 选中文字 99 | * 4. 内容变化了 100 | */ 101 | shouldComponentUpdate(nextProps, nextState) { 102 | return nextState.isPreview !== this.state.isPreview || nextState.onScrolled !== this.state.onScrolled || nextState.cs.render || nextState.cs.render !== this.state.cs.render || nextProps.value !== this.props.value; 103 | } 104 | 105 | componentWillUpdate(nextProps, nextState) { 106 | if (nextState.isPreview !== this.state.isPreview) { 107 | this.previewMarkup = marked(this._currentCodemirrorValue); 108 | } 109 | } 110 | 111 | componentWillUnmount() { 112 | if (this.codeMirror) { 113 | this.codeMirror.setValue(''); 114 | this.codeMirror.clearHistory(); 115 | // http://stackoverflow.com/questions/18828658/how-to-kill-a-codemirror-instance 116 | this.codeMirror.toTextArea(); 117 | } 118 | } 119 | 120 | getOptions() { 121 | const that = this; 122 | return Object.assign({ 123 | mode: 'gfm', 124 | lineNumbers: false, 125 | lineWrapping: true, 126 | indentWithTabs: true, 127 | matchBrackets: true, 128 | autofocus: true, 129 | tabSize: '2', 130 | extraKeys: { 131 | 'Enter': 'newlineAndIndentContinueMarkdownList', 132 | 'Cmd-S': () => { 133 | that.handleSave(); 134 | }, 135 | 'Cmd-B': () => { 136 | that.toggleFormat('bold'); 137 | }, 138 | 'Cmd-I': () => { 139 | that.toggleFormat('italic'); 140 | }, 141 | 'Cmd-1': () => { 142 | that.toggleFormat('h1'); 143 | }, 144 | 'Cmd-2': () => { 145 | that.toggleFormat('h2'); 146 | }, 147 | 'Cmd-3': () => { 148 | that.toggleFormat('h3'); 149 | }, 150 | 'Cmd-Alt-U': () => { 151 | that.toggleFormat('uList'); 152 | }, 153 | 'Cmd-Alt-O': () => { 154 | that.toggleFormat('oList'); 155 | }, 156 | 'Cmd-Alt-G': () => { 157 | that.toggleFormat('del'); 158 | }, 159 | 'Cmd-Alt-C': () => { 160 | that.toggleFormat('code'); 161 | }, 162 | 'Cmd-Alt-E': () => { 163 | that.toggleFormat('quote'); 164 | }, 165 | 'Cmd-Alt-L': () => { 166 | that.toggleFormat('link'); 167 | }, 168 | 'Cmd-Alt-P': () => { 169 | that.toggleFormat('image'); 170 | }, 171 | 'Cmd-0': () => { 172 | that.handlePreviewEvent(); 173 | }, 174 | }, 175 | }, this.props.options); 176 | } 177 | 178 | updateCursorState() { 179 | this.setState({ cs: getCursorState(this.codeMirror) }); 180 | } 181 | 182 | codemirrorValueChanged(doc) { 183 | const newValue = doc.getValue(); 184 | this._currentCodemirrorValue = newValue; 185 | } 186 | 187 | toggleFormat(formatKey) { 188 | if (this.state.isPreview) { 189 | return; 190 | } 191 | applyFormat(this.codeMirror, formatKey); 192 | } 193 | 194 | handleScrollEditor() { 195 | if (this.refs.editorContainer.scrollTop > 0 && !this.state.onScrolled) { 196 | this.setState({ onScrolled: true }); 197 | } else if (this.refs.editorContainer.scrollTop < 10) { 198 | this.setState({ onScrolled: false }); 199 | } 200 | } 201 | 202 | handleBack() { 203 | const { history, blob, params } = this.props; 204 | let backDir = ''; 205 | if (blob.loaded) { 206 | backDir = params.splat.split(blob.data.name)[0]; 207 | backDir = backDir !== '' ? 'd/' + backDir : ''; 208 | } 209 | history.pushState(null, '/_posts/' + backDir); 210 | } 211 | 212 | handlePenFocus() { 213 | event.preventDefault(); 214 | if (this.codeMirror) { 215 | this.codeMirror.focus(); 216 | } 217 | } 218 | 219 | handlePreviewEvent() { 220 | this.setState({ 221 | isPreview: !this.state.isPreview 222 | }); 223 | } 224 | 225 | handleSave() { 226 | const { handleSave } = this.props; 227 | // 重新组装 228 | if (handleSave) { 229 | handleSave(this._currentCodemirrorValue); 230 | } 231 | } 232 | 233 | handleUndo() { 234 | event.preventDefault(); 235 | if (this.codeMirror && !this.state.isPreview) { 236 | this.codeMirror.undoSelection(); 237 | } 238 | } 239 | 240 | handleRedo() { 241 | event.preventDefault(); 242 | if (this.codeMirror && !this.state.isPreview) { 243 | this.codeMirror.redoSelection(); 244 | } 245 | } 246 | 247 | renderIcon(icon) { 248 | return ; 249 | } 250 | 251 | renderButton(formatKey, label, action) { 252 | const { blob } = this.props; 253 | let actionTmp; 254 | if (!action) { 255 | actionTmp = this.toggleFormat.bind(this, formatKey); 256 | } else { 257 | actionTmp = action; 258 | } 259 | const className = classNames('leaf-editor-tool-icon', 260 | { 261 | 'leaf-editor-tool-icon-active': this.state.cs[formatKey], 262 | 'leaf-editor-tool-icon-disabled': this.state.isPreview || !blob.loaded 263 | }, 264 | ('leaf-editor-tool-icon-' + formatKey)); 265 | return ( 266 | 267 | 270 | 271 | ); 272 | } 273 | 274 | renderToolbar() { 275 | const { blob } = this.props; 276 | const previewClassName = classNames({ 277 | 'edit-icon': true, 278 | 'edit-icon-eyes': !this.state.isPreview, 279 | 'edit-icon-eyes-slash': this.state.isPreview 280 | }); 281 | 282 | const previewIconActive = classNames({ 283 | 'leaf-editor-tool-icon': true, 284 | 'leaf-editor-tool-icon-active': this.state.isPreview 285 | }); 286 | 287 | const historyClassName = classNames({ 288 | 'leaf-editor-tool-icon': true, 289 | 'leaf-editor-tool-icon-disabled': this.state.isPreview || !blob.loaded, 290 | }); 291 | 292 | return ( 293 |
    294 | 295 | 298 | 299 | 300 | 303 | 304 | 305 | {this.renderButton('h1', 'H1 (Cmd+1)')} 306 | {this.renderButton('h2', 'H2 (Cmd+2)')} 307 | {this.renderButton('h3', 'H3 (Cmd+3)')} 308 | 309 | {this.renderButton('bold', 'Blod (Cmd+B')} 310 | {this.renderButton('italic', 'Italic (Cmd+I)')} 311 | {this.renderButton('del', 'Strikethrough (Cmd+Alt+G)')} 312 | {this.renderButton('quote', 'Quote (Cmd+Alt+E)')} 313 | 314 | {this.renderButton('oList', 'Order List (Cmd+Alt+O)')} 315 | {this.renderButton('uList', 'Unorder List (Cmd+Alt+U)')} 316 | 317 | {this.renderButton('link', 'Link (Cmd+Alt+L)')} 318 | {this.renderButton('image', 'Image (Cmd+Alt+P)')} 319 | {this.renderButton('code', 'Code (Cmd+Alt+C)')} 320 | 321 | 322 | 325 | 326 |
    327 | ); 328 | } 329 | 330 | render() { 331 | const { className, blob } = this.props; 332 | 333 | const editorClassName = classNames({ 334 | 'leaf-editor-wrap': true, 335 | 'leaf-editor-wrap-preview': this.state.isPreview, 336 | [className]: className 337 | }); 338 | const editorToolClassName = classNames({ 339 | 'leaf-editor-tool': true, 340 | 'leaf-editor-tool-active': this.state.onScrolled, 341 | }); 342 | const penClassName = classNames({ 343 | 'pen': true, 344 | 'pen-hide': this.state.isPreview 345 | }); 346 | const viewClassName = classNames({ 347 | 'view': true, 348 | 'view-show': this.state.isPreview 349 | }); 350 | const containerClassName = classNames({ 351 | 'leaf-editor-container': true, 352 | 'clearfix': true, 353 | 'leaf-editor-container-preview': this.state.isPreview 354 | }); 355 | 356 | return ( 357 |
    358 |
    359 | {this.renderToolbar()} 360 |
    361 |
    362 |
    363 | { !blob.loaded && 364 |
    365 | { !blob.error && 366 | 367 | } 368 |
    369 | } 370 |
    371 |
    372 |