├── .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 - 伊布 [](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 |
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 |
41 |
42 |
43 | -
44 |
45 |
46 |
47 | {tree.data.length}
48 |
49 |
50 |
_posts
51 |
52 | -
53 |
54 |
55 |
56 | _drafts
57 |
58 | -
59 |
60 |
61 |
62 | _media
63 |
64 | -
65 |
66 |
67 |
68 | _setting
69 |
70 |
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 |
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 |
379 | {this.state.isPreview &&
380 |
383 | }
384 |
385 |
386 |
Build with Love in Eevee.
387 |
388 |
389 |
390 | );
391 | }
392 | }
393 |
394 | export default Editor;
395 |
--------------------------------------------------------------------------------
/src/components/Post/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 |
7 | import { Link } from 'react-router';
8 |
9 | class Head extends Component {
10 |
11 | static propTypes = {
12 | logout: PropTypes.func,
13 | handleRemove: PropTypes.func,
14 | handleEditMeta: PropTypes.func,
15 | blob: PropTypes.object,
16 | meta: PropTypes.object,
17 | params: PropTypes.object,
18 | }
19 |
20 | remove(e) {
21 | e.preventDefault();
22 | const { handleRemove } = this.props;
23 | handleRemove();
24 | }
25 |
26 | handleEditMeta(e) {
27 | e.preventDefault();
28 | const { handleEditMeta } = this.props;
29 | handleEditMeta();
30 | }
31 |
32 | render() {
33 | const { blob, meta, params } = this.props;
34 | // define backUrl
35 | let backDir = '';
36 | if (blob.loaded) {
37 | backDir = params.splat.split(blob.data.name)[0];
38 | backDir = backDir !== '' ? 'd/' + backDir : '';
39 | }
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | { blob.loaded &&
51 |
52 |
53 | {meta && meta.title}
54 |
55 |
56 | { meta.description &&
57 |
58 | Description:
59 | {meta.description}
60 |
61 | }
62 | { meta.categories &&
63 |
64 | Categories:
65 | {meta.categories}
66 |
67 | }
68 | { meta.tags &&
69 |
70 | Tags:
71 | {meta.tags}
72 |
73 | }
74 |
75 | Edit
76 |
77 |
78 |
79 | }
80 | { !blob.loaded &&
81 |
82 | { !blob.error &&
83 |
84 | }
85 | { blob.error &&
86 |
87 | }
88 |
89 | }
90 |
91 |
92 |
93 |
94 |
95 |
96 |
100 |
101 |
102 |
103 |
104 | );
105 | }
106 | }
107 |
108 | export default Head;
109 |
--------------------------------------------------------------------------------
/src/components/Post/Meta.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Form, Item as FormItem } from 'antd/lib/form';
3 | import Modal from 'antd/lib/modal';
4 | import Col from 'antd/lib/col';
5 | import Input from 'antd/lib/input';
6 | import Button from 'antd/lib/button';
7 |
8 | import createForm from 'rc-form/lib/createForm';
9 |
10 | class MetaForm extends Component {
11 |
12 | static propTypes = {
13 | modalVisible: PropTypes.bool,
14 | modalHandleOk: PropTypes.func,
15 | modalHandleCancel: PropTypes.func,
16 | metaData: PropTypes.object,
17 | form: PropTypes.object,
18 | }
19 |
20 | modalHandleOk(e) {
21 | e.preventDefault();
22 | const { form, modalHandleOk } = this.props;
23 | const { validateFields } = form;
24 | validateFields((error, values) => {
25 | if (!error) {
26 | modalHandleOk(values);
27 | }
28 | });
29 | }
30 |
31 | render() {
32 | const { modalVisible, modalHandleOk, modalHandleCancel, form, metaData } = this.props;
33 | const { getFieldProps, getFieldError } = form;
34 | return (
35 | Cancle,
42 |
45 | ]}
46 | >
47 |
152 |
153 | );
154 | }
155 | }
156 |
157 | export default createForm()(MetaForm);
158 |
--------------------------------------------------------------------------------
/src/constants/LeafActionTypes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PIZn on 16/1/11.
3 | */
4 |
5 | export const AUTH_LOGIN = 'AUTH_LOGIN';
6 | export const AUTH_LOGIN_SUCCESS = 'AUTH_LOGIN_SUCCESS';
7 | export const AUTH_LOGIN_FAIL = 'AUTH_LOGIN_FAIL';
8 | export const AUTH_LOGOUT = 'AUTH_LOGOUT';
9 | export const AUTH_LOGOUT_SUCCESS = 'AUTH_LOGOUT_SUCCESS';
10 | export const AUTH_LOGOUT_FAIL = 'AUTH_LOGOUT_FAIL';
11 | export const AUTH_LOGIN_DONE = 'AUTH_LOGIN_DONE';
12 |
13 | export const LOAD_USER_INFO = 'LOAD_USER_INFO';
14 | export const LOAD_USER_INFO_SUCCESS = 'LOAD_USER_INFO_SUCCESS';
15 | export const LOAD_USER_INFO_FAIL = 'LOAD_USER_INFO_FAIL';
16 | export const UPDATE_USER_INFO = 'UPDATE_USER_INFO';
17 |
18 | export const LOAD_REPO_INFO = 'LOAD_REPO_INFO';
19 | export const LOAD_REPO_INFO_SUCCESS = 'LOAD_REPO_INFO_SUCCESS';
20 | export const LOAD_REPO_INFO_FAIL = 'LOAD_REPO_INFO_FAIL';
21 | export const UPLOAD_REPO_INFO = 'UPLOAD_REPO_INFO';
22 | export const LOAD_REPO_TREE = 'LOAD_REPO_TREE';
23 | export const LOAD_REPO_TREE_SUCCESS = 'LOAD_REPO_TREE_SUCCESS';
24 | export const LOAD_REPO_TREE_FAIL = 'LOAD_REPO_TREE_FAIL';
25 |
26 | export const READ_REPO_TREE = 'READ_REPO_TREE';
27 | export const READ_REPO_TREE_SUCCESS = 'READ_REPO_TREE_SUCCESS';
28 | export const READ_REPO_TREE_FAIL = 'READ_REPO_TREE_FAIL';
29 | export const READ_REPO_BLOB = 'READ_REPO_BLOB';
30 | export const READ_REPO_BLOB_SUCCESS = 'READ_REPO_BLOB_SUCCESS';
31 | export const READ_REPO_BLOB_FAIL = 'READ_REPO_BLOB_FAIL';
32 | export const ADD_REPO_BLOB = 'ADD_REPO_BLOB';
33 | export const ADD_REPO_BLOB_SUCCESS = 'ADD_REPO_BLOB_SUCCESS';
34 | export const ADD_REPO_BLOB_FAIL = 'ADD_REPO_BLOB_FAIL';
35 | export const REMOVE_REPO_BLOB = 'REMOVE_REPO_BLOB';
36 | export const REMOVE_REPO_BLOB_SUCCESS = 'REMOVE_REPO_BLOB_SUCCESS';
37 | export const REMOVE_REPO_BLOB_FAIL = 'REMOVE_REPO_BLOB_FAIL';
38 |
39 | export const CLEAR_REPO_BLOB = 'CLEAR_REPO_BLOB';
40 |
41 | export const UPDATE_REPO_BLOB = 'UPDATE_REPO_BLOB';
42 | export const UPDATE_REPO_BLOB_SUCCESS = 'UPDATE_REPO_BLOB_SUCCESS';
43 | export const UPDATE_REPO_BLOB_FAIL = 'UPDATE_REPO_BLOB_FAIL';
44 |
45 | export const READ_REPO_BLOB_COMMIT = 'READ_REPO_BLOB_COMMIT';
46 | export const READ_REPO_BLOB_COMMIT_SUCCESS = 'READ_REPO_BLOB_COMMIT_SUCCESS';
47 | export const READ_REPO_BLOB_COMMIT_FAIL = 'READ_REPO_BLOB_COMMIT_FAIL';
48 |
--------------------------------------------------------------------------------
/src/containers/Desktop.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import * as actions from '../actions/LeafActions';
3 | import { connect } from 'react-redux';
4 |
5 | import message from 'antd/lib/message';
6 |
7 | import Head from '../components/Desktop/Head';
8 | import Aside from '../components/Desktop/Aside';
9 | import List from '../components/Desktop/List';
10 | import ModalAddFile from '../components/Desktop/AddFile';
11 |
12 | @connect(state => ({
13 | auth: state.auth,
14 | user: state.user,
15 | repoInfo: state.repoInfo,
16 | repoTree: state.repoTree,
17 | tree: state.tree,
18 | }))
19 |
20 | class Desktop extends Component {
21 |
22 | static propTypes = {
23 | dispatch: PropTypes.func,
24 | children: PropTypes.object,
25 | user: PropTypes.object,
26 | repoInfo: PropTypes.object,
27 | tree: PropTypes.object,
28 | history: PropTypes.object,
29 | }
30 |
31 | constructor(props) {
32 | super(props);
33 | this.state = {
34 | addFile: false,
35 | };
36 | }
37 |
38 | componentDidMount() {
39 | const { dispatch, user, repoInfo } = this.props;
40 | document.title = 'Desktop | Eevee';
41 | if (!user.loaded) {
42 | dispatch(actions.updateUserInfo())
43 | .then(() => {
44 | dispatch(actions.loadRepoInfo({
45 | username: this.props.user.data.login,
46 | }))
47 | .then(() => {
48 | const repo = {
49 | username: this.props.user.data.login,
50 | reponame: this.props.repoInfo.data.name,
51 | path: '_posts',
52 | };
53 | dispatch(actions.readRepoTree(repo));
54 | });
55 | });
56 | } else {
57 | if (!repoInfo.loaded) {
58 | dispatch(actions.loadRepoInfo({
59 | username: this.props.user.data.login,
60 | }))
61 | .then(() => {
62 | const repo = {
63 | username: this.props.user.data.login,
64 | reponame: this.props.repoInfo.data.name,
65 | path: '_posts',
66 | };
67 | dispatch(actions.readRepoTree(repo));
68 | });
69 | } else {
70 | const repo = {
71 | username: this.props.user.data.login,
72 | reponame: this.props.repoInfo.data.name,
73 | path: '_posts',
74 | };
75 | dispatch(actions.readRepoTree(repo));
76 | }
77 | }
78 | dispatch(actions.clearRepoBlob());
79 | }
80 | logout() {
81 | const { dispatch, history } = this.props;
82 | dispatch(actions.logout())
83 | .then(() => {
84 | history.pushState(null, 'login');
85 | });
86 | }
87 |
88 | handleAddFile(file) {
89 | event.preventDefault();
90 | const { dispatch, user, history, repoInfo } = this.props;
91 |
92 | const content = '---\n' +
93 | 'layout: post\n' +
94 | 'title: ' + file.title + '\n' +
95 | 'description: ' + file.description + '\n' +
96 | '---\n' +
97 | '# ';
98 |
99 | const repo = {
100 | username: user.data.login,
101 | email: user.data.email,
102 | reponame: repoInfo.data.name,
103 | path: '_posts/' + file.name,
104 | content,
105 | };
106 | this.setState({
107 | addFile: false,
108 | });
109 | const msg = message.loading('Saving...', 0);
110 | dispatch(actions.addRepoBlob(repo))
111 | .then(() => {
112 | msg();
113 | history.pushState(null, '_posts/f/' + file.name);
114 | });
115 | }
116 |
117 | handleShowAddModal() {
118 | this.setState({
119 | addFile: true,
120 | });
121 | }
122 |
123 | handleHideAddModal() {
124 | this.setState({
125 | addFile: false,
126 | });
127 | }
128 |
129 | render() {
130 | const { user, repoInfo, tree } = this.props;
131 |
132 | return (
133 |
134 |
135 |
141 |
142 |
143 |
147 |
150 |
155 |
156 |
157 |
158 |
159 | );
160 | }
161 | }
162 |
163 | export default Desktop;
164 |
--------------------------------------------------------------------------------
/src/containers/Dir.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import * as actions from '../actions/LeafActions';
3 | import { connect } from 'react-redux';
4 |
5 | import message from 'antd/lib/message';
6 |
7 | import Head from '../components/Desktop/Head';
8 | import Aside from '../components/Desktop/Aside';
9 | import List from '../components/Desktop/List';
10 | import ModalAddFile from '../components/Desktop/AddFile';
11 |
12 | @connect(state => ({
13 | auth: state.auth,
14 | user: state.user,
15 | repoInfo: state.repoInfo,
16 | repoTree: state.repoTree,
17 | tree: state.tree,
18 | }))
19 |
20 | class Dir extends Component {
21 |
22 | static propTypes = {
23 | dispatch: PropTypes.func,
24 | children: PropTypes.object,
25 | params: PropTypes.object,
26 | user: PropTypes.object,
27 | repoInfo: PropTypes.object,
28 | tree: PropTypes.object,
29 | history: PropTypes.object,
30 | }
31 |
32 | constructor(props) {
33 | super(props);
34 | this.state = {
35 | addFile: false,
36 | };
37 | }
38 |
39 | componentDidMount() {
40 | document.title = 'Directory | Eevee';
41 | this.getData();
42 | }
43 |
44 | componentDidUpdate(prevProps) {
45 | const oldId = prevProps.params.splat;
46 | const newId = this.props.params.splat;
47 | if (newId !== oldId) {
48 | this.getData();
49 | }
50 | }
51 |
52 | getData() {
53 | const { dispatch, user, repoInfo, params } = this.props;
54 | const { splat } = params;
55 | if (!user.loaded) {
56 | dispatch(actions.updateUserInfo())
57 | .then(() => {
58 | dispatch(actions.loadRepoInfo({
59 | username: this.props.user.data.login,
60 | }))
61 | .then(() => {
62 | const repo = {
63 | username: this.props.user.data.login,
64 | reponame: this.props.repoInfo.data.name,
65 | path: '_posts/' + splat,
66 | };
67 | dispatch(actions.readRepoTree(repo));
68 | });
69 | });
70 | } else {
71 | if (!repoInfo.loaded) {
72 | dispatch(actions.loadRepoInfo({
73 | username: this.props.user.data.login,
74 | }))
75 | .then(() => {
76 | const repo = {
77 | username: this.props.user.data.login,
78 | reponame: this.props.repoInfo.data.name,
79 | path: '_posts/' + params.splat,
80 | };
81 | dispatch(actions.readRepoTree(repo));
82 | });
83 | } else {
84 | const repo = {
85 | username: this.props.user.data.login,
86 | reponame: this.props.repoInfo.data.name,
87 | path: '_posts/' + params.splat,
88 | };
89 | dispatch(actions.readRepoTree(repo));
90 | }
91 | }
92 | dispatch(actions.clearRepoBlob());
93 | }
94 |
95 | logout() {
96 | const { dispatch, history } = this.props;
97 | dispatch(actions.logout())
98 | .then(() => {
99 | history.pushState(null, 'login');
100 | });
101 | }
102 |
103 | handleAddFile(file) {
104 | event.preventDefault();
105 | const { dispatch, user, history, repoInfo, params } = this.props;
106 |
107 | const content = '---\n' +
108 | 'layout: post\n' +
109 | 'title: ' + file.title + '\n' +
110 | 'description: ' + file.description + '\n' +
111 | '---\n' +
112 | '# ';
113 |
114 | const repo = {
115 | username: user.data.login,
116 | email: user.data.email,
117 | reponame: repoInfo.data.name,
118 | path: '_posts/' + params.splat + '/' + file.name,
119 | content,
120 | };
121 | this.setState({
122 | addFile: false
123 | });
124 | const msg = message.loading('Saving...', 0);
125 | dispatch(actions.addRepoBlob(repo))
126 | .then(() => {
127 | msg();
128 | history.pushState(null, '_posts/f/' + params.splat + '/' + file.name);
129 | });
130 | }
131 |
132 | handleShowAddModal() {
133 | this.setState({
134 | addFile: true
135 | });
136 | }
137 |
138 | handleHideAddModal() {
139 | this.setState({
140 | addFile: false,
141 | });
142 | }
143 |
144 | render() {
145 | const { user, repoInfo, tree } = this.props;
146 |
147 | return (
148 |
149 |
150 |
157 |
158 |
159 |
164 |
167 |
172 |
173 |
174 |
175 |
176 | );
177 | }
178 | }
179 |
180 | export default Dir;
181 |
--------------------------------------------------------------------------------
/src/containers/Login.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import * as actions from '../actions/LeafActions';
3 | import { connect } from 'react-redux';
4 | import classNames from 'classnames';
5 | import Button from 'antd/lib/button';
6 | import Icon from 'antd/lib/icon';
7 |
8 | import LoginForm from '../components/Login/LoginForm';
9 |
10 | @connect(state => ({
11 | auth: state.auth,
12 | user: state.user,
13 | repoInfo: state.repoInfo,
14 | }))
15 |
16 | class Login extends Component {
17 |
18 | static propTypes = {
19 | dispatch: PropTypes.func,
20 | auth: PropTypes.object,
21 | history: PropTypes.object,
22 | user: PropTypes.object,
23 | repoInfo: PropTypes.object,
24 | }
25 |
26 | constructor(props) {
27 | super(props);
28 |
29 | this.state = {
30 | noRepo: false,
31 | };
32 | }
33 |
34 | componentDidMount() {
35 | document.title = 'Connect | Eevee';
36 | }
37 |
38 | handleSubmit(data) {
39 | const { dispatch, history } = this.props;
40 | const that = this;
41 | dispatch(actions.login({
42 | email: data.email,
43 | pass: data.pass,
44 | })).then(() => {
45 | const { auth } = this.props;
46 | if (auth.loggedIn) {
47 | dispatch(actions.updateUserInfo(auth.user));
48 | const { user } = this.props;
49 | if (user.loaded) {
50 | dispatch(actions.loadRepoInfo({
51 | username: user.data.login,
52 | }))
53 | .then(() => {
54 | if (this.props.repoInfo.loaded) {
55 | dispatch(actions.loginDone());
56 | history.pushState(null, '/');
57 | } else {
58 | that.setState({
59 | noRepo: true,
60 | });
61 | }
62 | });
63 | }
64 | }
65 | });
66 | }
67 |
68 | logout() {
69 | const { dispatch } = this.props;
70 | const that = this;
71 | dispatch(actions.logout())
72 | .then(() => {
73 | that.setState({
74 | noRepo: false,
75 | });
76 | });
77 | }
78 |
79 | render() {
80 | const { auth, user, repoInfo } = this.props;
81 | const logoCls = classNames({
82 | 'head-logo': true,
83 | 'head-logo-loading': auth.loading || user.loading || repoInfo.loading,
84 | });
85 |
86 | return (
87 |
88 |
89 | { !this.state.noRepo &&
90 |
91 |
94 |
99 |
100 |
104 |
105 |
106 | }
107 | { this.state.noRepo &&
108 |
109 |
110 |
111 |

112 |
{user.data.name || user.data.login}
113 |
114 |
115 |
116 |
Hi, {user.data.name || user.data.login},
117 |
You may have the repo {user.data.login}.github.io
in your GitHub. Get the guide form GitHub Pages.
118 |
119 |
120 |
121 |
122 |
123 | }
124 |
125 |
Build with Love in Eevee.
126 |
127 |
128 |
129 | );
130 | }
131 | }
132 |
133 | export default Login;
134 |
--------------------------------------------------------------------------------
/src/containers/Post.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import * as actions from '../actions/LeafActions';
3 | import { connect } from 'react-redux';
4 |
5 | import yaml from 'js-yaml/lib/js-yaml';
6 |
7 | import Modal from 'antd/lib/modal';
8 | import message from 'antd/lib/message';
9 |
10 | import Head from '../components/Post/Head';
11 | import Editor from '../components/Post/Editor';
12 | import MetaForm from '../components/Post/Meta';
13 |
14 | const confirm = Modal.confirm;
15 |
16 | @connect(state => ({
17 | auth: state.auth,
18 | user: state.user,
19 | repoInfo: state.repoInfo,
20 | blob: state.blob,
21 | }))
22 |
23 | class Post extends Component {
24 |
25 | static propTypes = {
26 | params: PropTypes.object,
27 | dispatch: PropTypes.func,
28 | user: PropTypes.object,
29 | repoInfo: PropTypes.object,
30 | blob: PropTypes.object,
31 | history: PropTypes.object,
32 | }
33 |
34 | constructor(props) {
35 | super(props);
36 | this.state = {
37 | meta: {},
38 | head: '',
39 | body: '',
40 | editMeta: false,
41 | };
42 | }
43 |
44 | componentDidMount() {
45 | const { params, dispatch, user, repoInfo } = this.props;
46 |
47 | document.title = 'Edit post | Eevee';
48 |
49 | const that = this;
50 | if (!user.loaded) {
51 | dispatch(actions.updateUserInfo())
52 | .then(() => {
53 | dispatch(actions.loadRepoInfo({
54 | username: this.props.user.data.login,
55 | }))
56 | .then(() => {
57 | const repo = {
58 | username: this.props.user.data.login,
59 | reponame: this.props.repoInfo.data.name,
60 | path: '_posts/' + params.splat,
61 | };
62 | dispatch(actions.readRepoBlob(repo)).then(() => {
63 | that.generateContent();
64 | });
65 | });
66 | });
67 | } else {
68 | if (!repoInfo.loaded) {
69 | dispatch(actions.loadRepoInfo({
70 | username: this.props.user.data.login,
71 | }))
72 | .then(() => {
73 | const repo = {
74 | username: this.props.user.data.login,
75 | reponame: this.props.repoInfo.data.name,
76 | path: '_posts/' + params.splat,
77 | };
78 | dispatch(actions.readRepoBlob(repo)).then(() => {
79 | that.generateContent();
80 | });
81 | });
82 | } else {
83 | const repo = {
84 | username: this.props.user.data.login,
85 | reponame: this.props.repoInfo.data.name,
86 | path: '_posts/' + params.splat,
87 | };
88 | dispatch(actions.readRepoBlob(repo)).then(() => {
89 | that.generateContent();
90 | });
91 | }
92 | }
93 | }
94 |
95 | handleSaveMeta(data) {
96 | if (data === this.state.head) {
97 | console.log('[log]: 没有修改');
98 | return false;
99 | }
100 | this.setState({
101 | meta: yaml.safeLoad(data),
102 | head: data,
103 | });
104 | const cnt = data + '\n---\n' + this.state.body;
105 | this.handleSave(cnt);
106 | }
107 |
108 | handleSaveCnt(data) {
109 | if (data === this.state.body) {
110 | console.log('[log]: 没有修改');
111 | return false;
112 | }
113 |
114 | this.setState({
115 | body: data,
116 | });
117 |
118 | const cnt = this.state.head + '\n---\n' + data;
119 | this.handleSave(cnt);
120 | }
121 |
122 | handleSave(cnt) {
123 | const { blob, dispatch, user, repoInfo, params } = this.props;
124 |
125 | if (blob.updating) {
126 | console.log('[log]: 文档正在保存...');
127 | return false;
128 | }
129 |
130 | const repo = {
131 | username: user.data.login,
132 | email: user.data.email,
133 | reponame: repoInfo.data.name,
134 | path: '_posts/' + params.splat,
135 | content: cnt,
136 | };
137 |
138 | const msg = message.loading('Saving...', 0);
139 | dispatch(actions.updateRepoBlob(repo))
140 | .then(() => {
141 | msg();
142 | });
143 | }
144 |
145 | handleFocusChange() {
146 | }
147 |
148 | handleUpdateCode() {
149 | }
150 |
151 | handleRemove() {
152 | const { params } = this.props;
153 | const that = this;
154 | confirm({
155 | title: 'Remove this post',
156 | content: 'The file "' + params.splat + '" will be remove forever.',
157 | onOk: () => {
158 | that.handleRemoveReques();
159 | },
160 | onCancel: () => {}
161 | });
162 | }
163 |
164 | handleRemoveReques() {
165 | const { dispatch, user, params, repoInfo, history, blob } = this.props;
166 | let backDir;
167 | if (blob.loaded) {
168 | backDir = params.splat.split(blob.data.name)[0];
169 | backDir = backDir !== '' ? 'd/' + backDir : '';
170 | }
171 |
172 | const repo = {
173 | username: user.data.login,
174 | reponame: repoInfo.data.name,
175 | path: '_posts/' + params.splat
176 | };
177 | const msg = message.loading('Removing...', 0);
178 |
179 | dispatch(actions.removeRepoBlob(repo))
180 | .then(() => {
181 | msg();
182 | // message.success('删除成功');
183 | })
184 | .then(() => {
185 | history.pushState(null, '_posts/' + backDir);
186 | });
187 | }
188 |
189 | handleEditMeta() {
190 | this.setState({
191 | editMeta: true,
192 | });
193 | }
194 |
195 | handleEditMetaSubmit(data) {
196 | this.setState({
197 | editMeta: false,
198 | });
199 | let tmpData = '---\n' +
200 | 'layout: ' + data.layout + '\n' +
201 | 'title: ' + data.title + '\n' +
202 | 'description: ' + data.description + '\n';
203 | if (data.categories) {
204 | tmpData += 'categories: ' + data.categories + '\n';
205 | }
206 | if (data.tags) {
207 | tmpData += 'tags: ' + data.tags + '\n';
208 | }
209 | this.handleSaveMeta(tmpData);
210 | }
211 |
212 | handleEditMetaCancel() {
213 | this.setState({
214 | editMeta: false,
215 | });
216 | }
217 |
218 | splitInput(str) {
219 | if (str.slice(0, 3) !== '---') {
220 | return;
221 | }
222 | const matcher = /\n(\.{3}|-{3})\n/g;
223 | const metaEnd = matcher.exec(str);
224 | return metaEnd && [str.slice(0, metaEnd.index), str.slice(matcher.lastIndex)];
225 | }
226 |
227 | metaMarked(src) {
228 | const mySplitInput = this.splitInput(src);
229 | return mySplitInput ? {
230 | meta: yaml.safeLoad(mySplitInput[0]),
231 | head: mySplitInput[0],
232 | body: mySplitInput[1]
233 | } : {
234 | meta: null,
235 | head: null,
236 | body: src
237 | };
238 | }
239 |
240 | generateContent() {
241 | const { blob } = this.props;
242 | if (blob.loaded) {
243 | const defaultValue = this.metaMarked(blob.data.content);
244 | // update meta and head
245 | this.setState({
246 | meta: defaultValue.meta,
247 | head: defaultValue.head,
248 | body: defaultValue.body,
249 | });
250 | } else {
251 | if (blob.error) {
252 | const msg = blob.error.request && blob.error.request.message;
253 | message.error(msg || 'Something error');
254 | }
255 | }
256 | }
257 |
258 | render() {
259 | const { blob } = this.props;
260 | return (
261 |
262 |
263 |
270 |
278 |
284 |
285 |
286 | );
287 | }
288 | }
289 |
290 | export default Post;
291 |
--------------------------------------------------------------------------------
/src/entry/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import { Router, Route, Redirect } from 'react-router';
5 | import createHistory from 'history/lib/createHashHistory';
6 |
7 | import configureStore from '../stores';
8 | import { Provider } from 'react-redux';
9 |
10 | import Login from '../containers/Login';
11 | import Desktop from '../containers/Desktop';
12 | import Post from '../containers/Post';
13 | import Dir from '../containers/Dir';
14 |
15 | import auth from '../services/auth';
16 | import '../common/lib';
17 |
18 | const history = createHistory({
19 | queryKey: false,
20 | });
21 |
22 | const store = configureStore();
23 |
24 | function requireAuth(nextState, replace) {
25 | if (!auth.loggedIn()) {
26 | replace({ nextPathname: nextState.location.pathname }, '/login', nextState.location.query);
27 | }
28 | }
29 |
30 | ReactDOM.render(
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ,
40 | document.getElementById('leaf')
41 | );
42 |
--------------------------------------------------------------------------------
/src/middleware/index.js:
--------------------------------------------------------------------------------
1 | export { default as promiseMiddleware } from '/promiseMiddleware';
2 |
--------------------------------------------------------------------------------
/src/middleware/promiseMiddleware.js:
--------------------------------------------------------------------------------
1 | // Middleware
2 | export default function promiseMiddleware() {
3 | return (next) => (action) => {
4 | const { promise, types, ...rest } = action;
5 | if (!promise) {
6 | return next(action);
7 | }
8 | const [REQUEST, SUCCESS, FAILURE] = types;
9 | next({ ...rest, type: REQUEST });
10 | return promise.then(
11 | (result) => {
12 | next({ ...rest, result, type: SUCCESS });
13 | },
14 | (error) => {
15 | next({ ...rest, error, type: FAILURE });
16 | }
17 | );
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/src/reducers/Blob.js:
--------------------------------------------------------------------------------
1 | import { READ_REPO_BLOB, READ_REPO_BLOB_SUCCESS, READ_REPO_BLOB_FAIL, CLEAR_REPO_BLOB, UPDATE_REPO_BLOB, UPDATE_REPO_BLOB_SUCCESS, UPDATE_REPO_BLOB_FAIL, REMOVE_REPO_BLOB, REMOVE_REPO_BLOB_SUCCESS, REMOVE_REPO_BLOB_FAIL, ADD_REPO_BLOB, ADD_REPO_BLOB_SUCCESS, ADD_REPO_BLOB_FAIL, AUTH_LOGOUT } from '../constants/LeafActionTypes';
2 | import assign from 'object-assign';
3 |
4 | const initialState = {
5 | loaded: false,
6 | data: {},
7 | };
8 |
9 | export default function blob(state = initialState, action) {
10 | switch (action.type) {
11 | case READ_REPO_BLOB:
12 | return {
13 | ...state,
14 | loading: true,
15 | loaded: false,
16 | };
17 | case READ_REPO_BLOB_SUCCESS:
18 | return {
19 | ...state,
20 | loading: false,
21 | loaded: true,
22 | data: action.result,
23 | error: action.error,
24 | };
25 | case READ_REPO_BLOB_FAIL:
26 | return {
27 | ...state,
28 | loading: false,
29 | loaded: false,
30 | error: action.error,
31 | };
32 | case CLEAR_REPO_BLOB:
33 | return {
34 | loaded: false,
35 | data: {},
36 | loading: false,
37 | error: action.error,
38 | };
39 | case UPDATE_REPO_BLOB:
40 | return {
41 | ...state,
42 | data: assign({}, state.data, {
43 | content: action.data.content,
44 | }),
45 | updating: true,
46 | };
47 | case UPDATE_REPO_BLOB_SUCCESS:
48 | return {
49 | ...state,
50 | updating: false,
51 | updated: true,
52 | };
53 | case UPDATE_REPO_BLOB_FAIL:
54 | return {
55 | ...state,
56 | updating: false,
57 | updated: false,
58 | };
59 | case REMOVE_REPO_BLOB:
60 | return {
61 | ...state,
62 | removing: true,
63 | };
64 | case REMOVE_REPO_BLOB_SUCCESS:
65 | return {
66 | ...state,
67 | removing: false,
68 | removed: true,
69 | data: {},
70 | };
71 | case REMOVE_REPO_BLOB_FAIL:
72 | return {
73 | ...state,
74 | removing: false,
75 | removed: false,
76 | };
77 | case ADD_REPO_BLOB:
78 | return {
79 | ...state,
80 | adding: true,
81 | };
82 | case ADD_REPO_BLOB_SUCCESS:
83 | return {
84 | ...state,
85 | adding: false,
86 | added: true,
87 | data: action.result.content,
88 | error: action.error,
89 | };
90 | case ADD_REPO_BLOB_FAIL:
91 | return {
92 | ...state,
93 | adding: false,
94 | added: false,
95 | error: action.error
96 | };
97 | case AUTH_LOGOUT:
98 | return {
99 | loaded: false,
100 | data: {},
101 | };
102 | default:
103 | return state;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/reducers/RepoInfo.js:
--------------------------------------------------------------------------------
1 | import { LOAD_REPO_INFO, LOAD_REPO_INFO_SUCCESS, LOAD_REPO_INFO_FAIL, UPLOAD_REPO_INFO, AUTH_LOGOUT } from '../constants/LeafActionTypes';
2 |
3 | const initialState = {
4 | loaded: false,
5 | data: {},
6 | };
7 |
8 | export default function repoInfo(state = initialState, action) {
9 | switch (action.type) {
10 | case LOAD_REPO_INFO:
11 | return {
12 | ...state,
13 | loading: true,
14 | loaded: false,
15 | };
16 | case LOAD_REPO_INFO_SUCCESS:
17 | return {
18 | ...state,
19 | loading: false,
20 | loaded: true,
21 | data: action.result,
22 | error: action.error,
23 | };
24 | case LOAD_REPO_INFO_FAIL:
25 | return {
26 | ...state,
27 | loading: false,
28 | loaded: false,
29 | error: action.error,
30 | };
31 | case UPLOAD_REPO_INFO:
32 | return {
33 | ...state,
34 | loading: false,
35 | loaded: true,
36 | data: action.data,
37 | };
38 | case AUTH_LOGOUT:
39 | return {
40 | loaded: false,
41 | data: {},
42 | };
43 | default:
44 | return state;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/reducers/RepoTree.js:
--------------------------------------------------------------------------------
1 | import { LOAD_REPO_TREE, LOAD_REPO_TREE_SUCCESS, LOAD_REPO_TREE_FAIL, AUTH_LOGOUT } from '../constants/LeafActionTypes';
2 |
3 | const initialState = {
4 | loaded: false,
5 | data: {},
6 | };
7 |
8 | export default function repoTree(state = initialState, action) {
9 | switch (action.type) {
10 | case LOAD_REPO_TREE:
11 | return {
12 | ...state,
13 | loading: true,
14 | loaded: false,
15 | };
16 | case LOAD_REPO_TREE_SUCCESS:
17 | return {
18 | ...state,
19 | loading: false,
20 | loaded: true,
21 | data: action.result,
22 | error: action.error,
23 | };
24 | case LOAD_REPO_TREE_FAIL:
25 | return {
26 | ...state,
27 | loading: false,
28 | loaded: false,
29 | error: action.error,
30 | };
31 | case AUTH_LOGOUT:
32 | return {
33 | loaded: false,
34 | data: {},
35 | };
36 | default:
37 | return state;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/reducers/Tree.js:
--------------------------------------------------------------------------------
1 | import { READ_REPO_TREE, READ_REPO_TREE_SUCCESS, READ_REPO_TREE_FAIL, ADD_REPO_BLOB, ADD_REPO_BLOB_SUCCESS, ADD_REPO_BLOB_FAIL, AUTH_LOGOUT, REMOVE_REPO_BLOB } from '../constants/LeafActionTypes';
2 |
3 | const initialState = {
4 | loaded: false,
5 | data: [],
6 | };
7 |
8 | export default function tree(state = initialState, action) {
9 | switch (action.type) {
10 | case READ_REPO_TREE:
11 | return {
12 | ...state,
13 | loading: true,
14 | loaded: false,
15 | };
16 | case READ_REPO_TREE_SUCCESS:
17 | return {
18 | ...state,
19 | loading: false,
20 | loaded: true,
21 | data: action.result,
22 | error: action.error,
23 | };
24 | case READ_REPO_TREE_FAIL:
25 | return {
26 | ...state,
27 | loading: false,
28 | loaded: false,
29 | error: action.error,
30 | };
31 | case ADD_REPO_BLOB:
32 | return {
33 | ...state,
34 | adding: true,
35 | };
36 | case ADD_REPO_BLOB_SUCCESS:
37 | return {
38 | ...state,
39 | adding: false,
40 | added: true,
41 | data: state.data.concat([action.result.content]),
42 | };
43 | case ADD_REPO_BLOB_FAIL:
44 | return {
45 | ...state,
46 | adding: false,
47 | added: false,
48 | error: action.error,
49 | };
50 | case AUTH_LOGOUT:
51 | return {
52 | loaded: false,
53 | data: [],
54 | };
55 | case REMOVE_REPO_BLOB:
56 | return {
57 | ...state,
58 | data: state.data.filter(item => {
59 | return item.path !== action.data.path;
60 | }),
61 | };
62 | default:
63 | return state;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/reducers/User.js:
--------------------------------------------------------------------------------
1 | import { LOAD_USER_INFO, LOAD_USER_INFO_SUCCESS, LOAD_USER_INFO_FAIL, UPDATE_USER_INFO, AUTH_LOGOUT } from '../constants/LeafActionTypes';
2 |
3 | const initialState = {
4 | loaded: false,
5 | data: {},
6 | };
7 |
8 | export default function user(state = initialState, action) {
9 | switch (action.type) {
10 | case LOAD_USER_INFO:
11 | return {
12 | ...state,
13 | loading: true,
14 | };
15 | case LOAD_USER_INFO_SUCCESS:
16 | return {
17 | ...state,
18 | loading: false,
19 | loaded: true,
20 | data: action.result,
21 | error: action.error,
22 | };
23 | case LOAD_USER_INFO_FAIL:
24 | return {
25 | ...state,
26 | loading: false,
27 | loaded: false,
28 | error: action.error,
29 | };
30 | case UPDATE_USER_INFO:
31 | return {
32 | ...state,
33 | loading: false,
34 | loaded: true,
35 | data: action.data,
36 | error: null,
37 | };
38 | case AUTH_LOGOUT:
39 | return {
40 | loaded: false,
41 | data: {},
42 | };
43 | default:
44 | return state;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import { AUTH_LOGIN, AUTH_LOGIN_SUCCESS, AUTH_LOGIN_FAIL, AUTH_LOGOUT, AUTH_LOGOUT_SUCCESS, AUTH_LOGOUT_FAIL } from '../constants/LeafActionTypes';
2 | import storage from '../utils/localStorage';
3 |
4 | const _leafAdmin = storage.get('_leafAdmin');
5 |
6 | const initialState = {
7 | loggedIn: false,
8 | email: _leafAdmin && _leafAdmin.email,
9 | pass: _leafAdmin && _leafAdmin.pass,
10 | };
11 |
12 | export default function auth(state = initialState, action) {
13 | switch (action.type) {
14 | case AUTH_LOGIN:
15 | return {
16 | ...state,
17 | loading: true,
18 | };
19 | case AUTH_LOGIN_SUCCESS:
20 | return {
21 | ...state,
22 | loading: false,
23 | loggedIn: true,
24 | email: action.data.email,
25 | pass: action.data.pass,
26 | user: action.result,
27 | error: action.error,
28 | };
29 | case AUTH_LOGIN_FAIL:
30 | return {
31 | ...state,
32 | loading: false,
33 | loggedIn: false,
34 | error: action.error,
35 | };
36 | case AUTH_LOGOUT:
37 | return {
38 | ...state,
39 | loading: true,
40 | email: '',
41 | pass: '',
42 | loggedIn: false,
43 | };
44 | case AUTH_LOGOUT_SUCCESS:
45 | return {
46 | ...state,
47 | loading: false,
48 | };
49 | case AUTH_LOGOUT_FAIL:
50 | return {
51 | ...state,
52 | loading: false,
53 | };
54 | default:
55 | return state;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | export { default as auth } from './Auth';
2 | export { default as user } from './User';
3 | export { default as repoInfo } from './RepoInfo';
4 | export { default as repoTree } from './RepoTree';
5 | export { default as tree } from './Tree';
6 | export { default as blob } from './Blob';
7 |
--------------------------------------------------------------------------------
/src/services/auth.js:
--------------------------------------------------------------------------------
1 | import storage from '../utils/localStorage';
2 | import Github from 'github-api/dist/github.min';
3 | import assign from 'object-assign';
4 |
5 | const authorization = {
6 |
7 | login(auth) {
8 | return new Promise((resolve, reject) => {
9 | const github = new Github({
10 | username: auth.email,
11 | password: auth.pass,
12 | auth: 'basic'
13 | });
14 | const user = github.getUser();
15 | user.show(null, (err, repodata) => {
16 | if (err) {
17 | reject(err);
18 | } else {
19 | storage.set('_leafAdmin', {
20 | email: auth.email,
21 | pass: auth.pass,
22 | loggedIn: false,
23 | });
24 | resolve(repodata);
25 | }
26 | });
27 | });
28 | },
29 |
30 | loginDone() {
31 | let _leafAdmin = storage.get('_leafAdmin');
32 | _leafAdmin = assign({}, _leafAdmin, {
33 | loggedIn: true,
34 | });
35 | storage.set('_leafAdmin', _leafAdmin);
36 | },
37 |
38 | logout() {
39 | return new Promise((resolve) => {
40 | storage.remove('_leafAdmin');
41 | resolve();
42 | });
43 | },
44 |
45 | loggedIn() {
46 | const admin = storage.get('_leafAdmin');
47 | if (admin.loggedIn) {
48 | return true;
49 | }
50 | return false;
51 | },
52 | };
53 |
54 | module.exports = authorization;
55 |
--------------------------------------------------------------------------------
/src/services/repo.js:
--------------------------------------------------------------------------------
1 | import storage from '../utils/localStorage';
2 | import Github from 'github-api/dist/github.min';
3 |
4 | const b64decode = (string) => {
5 | const base64Decode = require('base-64').decode;
6 | const utf8Decode = require('utf8').decode;
7 | return utf8Decode(base64Decode(string));
8 | };
9 |
10 | const Repo = {
11 |
12 | init() {
13 | const _leafAdmin = storage.get('_leafAdmin');
14 | const github = new Github({
15 | username: _leafAdmin.email,
16 | password: _leafAdmin.pass,
17 | auth: 'basic',
18 | });
19 | this.github = github;
20 | },
21 |
22 | getInfo(username, reponame) {
23 | const repo = this.github.getRepo(username, reponame);
24 | return new Promise((resolve, reject) => {
25 | repo.show((err, repoData) => {
26 | if (err) {
27 | reject(err);
28 | } else {
29 | resolve(repoData);
30 | }
31 | });
32 | });
33 | },
34 |
35 | getTree(username, reponame) {
36 | const repo = this.github.getRepo(username, reponame);
37 | return new Promise((resolve, reject) => {
38 | repo.getTree('master', (err, treeData) => {
39 | if (err) {
40 | reject(err);
41 | } else {
42 | resolve(treeData);
43 | }
44 | });
45 | });
46 | },
47 |
48 | readTree(username, reponame, path) {
49 | const repo = this.github.getRepo(username, reponame);
50 | return new Promise((resolve, reject) => {
51 | repo.read('master', path, (err, file) => {
52 | if (err) {
53 | reject(err);
54 | } else {
55 | const tree = file.reverse();
56 | resolve(tree);
57 | }
58 | });
59 | });
60 | },
61 |
62 | readBlob(username, reponame, path) {
63 | const repo = this.github.getRepo(username, reponame);
64 | return new Promise((resolve, reject) => {
65 | repo.contents('master', path, (err, file) => {
66 | if (err) {
67 | reject(err);
68 | } else {
69 | file.content = b64decode(file.content);
70 | resolve(file);
71 | }
72 | });
73 | });
74 | },
75 |
76 | addBlob(data) {
77 | const repo = this.github.getRepo(data.username, data.reponame);
78 | const options = {
79 | author: {
80 | name: data.username,
81 | email: data.email,
82 | },
83 | committer: {
84 | name: data.username,
85 | email: data.email,
86 | },
87 | };
88 |
89 | return new Promise((resolve, reject) => {
90 | repo.write('master', data.path, data.content, '[log]: Add post', options, (err, file) => {
91 | if (err) {
92 | reject(err);
93 | } else {
94 | resolve(file);
95 | }
96 | });
97 | });
98 | },
99 |
100 | readBlobCommit(username, reponame, sha) {
101 | const repo = this.github.getRepo(username, reponame);
102 | return new Promise((resolve, reject) => {
103 | repo.getCommit('master', sha, (err, commit) => {
104 | if (err) {
105 | reject(err);
106 | } else {
107 | resolve(commit);
108 | }
109 | });
110 | });
111 | },
112 |
113 | writeBlob(data) {
114 | const repo = this.github.getRepo(data.username, data.reponame);
115 | const options = {
116 | author: {
117 | name: data.username,
118 | email: data.email
119 | },
120 | committer: {
121 | name: data.username,
122 | email: data.email
123 | },
124 | };
125 | return new Promise((resolve, reject) => {
126 | repo.write('master', data.path, data.content, '[log]: Update post', options, (err, file) => {
127 | if (err) {
128 | reject(err);
129 | } else {
130 | resolve(file);
131 | }
132 | });
133 | });
134 | },
135 |
136 | removeBlob(data) {
137 | const repo = this.github.getRepo(data.username, data.reponame);
138 | return new Promise((resolve, reject) => {
139 | repo.remove('master', data.path, (err, file) => {
140 | if (err) {
141 | reject(err);
142 | } else {
143 | resolve(file);
144 | }
145 | });
146 | });
147 | }
148 | };
149 |
150 | Repo.init();
151 | module.exports = Repo;
152 |
--------------------------------------------------------------------------------
/src/services/user.js:
--------------------------------------------------------------------------------
1 | import storage from '../utils/localStorage';
2 | import Github from 'github-api/dist/github.min';
3 |
4 | const User = {
5 |
6 | init() {
7 | const _leafAdmin = storage.get('_leafAdmin');
8 | const github = new Github({
9 | username: _leafAdmin.email,
10 | password: _leafAdmin.pass,
11 | auth: 'basic',
12 | });
13 | this.github = github;
14 | },
15 |
16 | getInfo() {
17 | const user = this.github.getUser();
18 | return new Promise((resolve, reject) => {
19 | user.show(null, (err, userData) => {
20 | if (err) {
21 | reject(err);
22 | } else {
23 | resolve(userData);
24 | }
25 | });
26 | });
27 | },
28 |
29 | checkRepo(username) {
30 | const user = this.github.getUser();
31 | var options = {
32 | type: 'owner',
33 | sort: 'updated',
34 | per_page: 1000,
35 | page: 1,
36 | };
37 | const rule = username + '.github.';
38 | return new Promise((resolve, reject) => {
39 | user.repos(options, (err, repos) => {
40 | if (err) {
41 | reject(err);
42 | } else {
43 | let status = false;
44 | let result;
45 | repos.map(item => {
46 | if (item.name.indexOf(rule) === 0) {
47 | status = true;
48 | result = item;
49 | }
50 | });
51 | if (status) {
52 | resolve(result);
53 | } else {
54 | reject({
55 | error: 404,
56 | message: 'Not found',
57 | });
58 | }
59 | }
60 | });
61 | });
62 | }
63 | };
64 |
65 | User.init();
66 | module.exports = User;
67 |
--------------------------------------------------------------------------------
/src/stores/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
2 | import promiseMiddleware from '../middleware/promiseMiddleware';
3 | import * as reducers from '../reducers/index';
4 | import thunk from 'redux-thunk';
5 | const finalCreateStore = compose(
6 | applyMiddleware(thunk, promiseMiddleware)
7 | )(createStore);
8 |
9 | const larkReducer = combineReducers(reducers);
10 |
11 | export default function configureStore(initialState) {
12 | return finalCreateStore(larkReducer, initialState);
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/CodeMirror.less:
--------------------------------------------------------------------------------
1 | /* BASICS */
2 |
3 | .CodeMirror {
4 | text-size-adjust: 100%;
5 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
6 | font-size: 14px;
7 | line-height: 1.6;
8 | word-wrap: break-word;
9 | height: 100%;
10 | min-height: 100%;
11 | position: relative;
12 | overflow: hidden;
13 | color: #333;
14 | background: #fff;
15 | }
16 |
17 | /* PADDING */
18 |
19 | .CodeMirror-lines {
20 | }
21 | .CodeMirror pre {
22 | }
23 |
24 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
25 | background-color: white; /* The little square between H and V scrollbars */
26 | }
27 |
28 | /* GUTTER */
29 | .CodeMirror-gutters {
30 | border-right: 1px solid #ddd;
31 | background-color: #f7f7f7;
32 | white-space: nowrap;
33 | }
34 | .CodeMirror-linenumbers {}
35 | .CodeMirror-linenumber {
36 | padding: 0 3px 0 5px;
37 | min-width: 20px;
38 | text-align: right;
39 | color: #999;
40 | white-space: nowrap;
41 | }
42 |
43 | .CodeMirror-guttermarker { color: black; }
44 | .CodeMirror-guttermarker-subtle { color: #999; }
45 |
46 | /* CURSOR */
47 |
48 | .CodeMirror div.CodeMirror-cursor {
49 | border-left: 1px solid black;
50 | }
51 | /* Shown when moving in bi-directional text */
52 | .CodeMirror div.CodeMirror-secondarycursor {
53 | border-left: 1px solid silver;
54 | }
55 | .CodeMirror.cm-fat-cursor div.CodeMirror-cursor {
56 | width: auto;
57 | border: 0;
58 | background: #7e7;
59 | }
60 | .CodeMirror.cm-fat-cursor div.CodeMirror-cursors {
61 | z-index: 1;
62 | }
63 |
64 | .cm-animate-fat-cursor {
65 | width: auto;
66 | border: 0;
67 | -webkit-animation: blink 1.06s steps(1) infinite;
68 | -moz-animation: blink 1.06s steps(1) infinite;
69 | animation: blink 1.06s steps(1) infinite;
70 | }
71 | @-moz-keyframes blink {
72 | 0% { background: #7e7; }
73 | 50% { background: none; }
74 | 100% { background: #7e7; }
75 | }
76 | @-webkit-keyframes blink {
77 | 0% { background: #7e7; }
78 | 50% { background: none; }
79 | 100% { background: #7e7; }
80 | }
81 | @keyframes blink {
82 | 0% { background: #7e7; }
83 | 50% { background: none; }
84 | 100% { background: #7e7; }
85 | }
86 |
87 | /* Can style cursor different in overwrite (non-insert) mode */
88 | div.CodeMirror-overwrite div.CodeMirror-cursor {}
89 |
90 | .cm-tab { display: inline-block; text-decoration: inherit; }
91 |
92 | .CodeMirror-ruler {
93 | border-left: 1px solid #ccc;
94 | position: absolute;
95 | }
96 |
97 | /* DEFAULT THEME */
98 |
99 | .cm-s-default .cm-quote {color: #777;}
100 | .cm-negative {color: #d44;}
101 | .cm-positive {color: #292;}
102 | .cm-header, .cm-strong {font-weight: bold;}
103 | .cm-em {font-style: italic;}
104 | .cm-url {text-decoration: underline;}
105 | .cm-strikethrough {text-decoration: line-through;}
106 |
107 | .cm-s-default .cm-keyword {color: #708;}
108 | .cm-s-default .cm-atom {color: #219;}
109 | .cm-s-default .cm-number {color: #3b88ff;}
110 | .cm-s-default .cm-def {color: #2984ff;}
111 | .cm-s-default .cm-variable,
112 | .cm-s-default .cm-punctuation,
113 | .cm-s-default .cm-property,
114 | .cm-s-default .cm-operator {}
115 | .cm-s-default .cm-variable-2 {color: #333;}
116 | .cm-s-default .cm-variable-3 {color: #666;}
117 | .cm-s-default .cm-comment {
118 | color: #8e908c;
119 | }
120 | .cm-s-default .cm-string {color: #a11;}
121 | .cm-s-default .cm-string-2 {color: #f50;}
122 | .cm-s-default .cm-meta {color: #555;}
123 | .cm-s-default .cm-qualifier {color: #555;}
124 | .cm-s-default .cm-builtin {color: #30a;}
125 | .cm-s-default .cm-bracket {color: #997;}
126 | .cm-s-default .cm-tag {
127 | color: #718c00;
128 | }
129 | .cm-s-default .cm-attribute {color: #00c;}
130 | .cm-s-default .cm-hr {color: #999;}
131 | .cm-s-default .cm-link {color: #4078c0;}
132 | .cm-s-default .cm-url { color: #4078c0; }
133 |
134 | .cm-s-default .cm-error {color: #f00;}
135 | .cm-invalidchar {color: #f00;}
136 |
137 | .CodeMirror-composing { border-bottom: 2px solid; }
138 |
139 | /* Default styles for common addons */
140 |
141 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
142 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
143 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
144 | .CodeMirror-activeline-background {background: #e8f2ff;}
145 |
146 | /* STOP */
147 |
148 | /* The rest of this file contains styles related to the mechanics of
149 | the editor. You probably shouldn't touch them. */
150 | .CodeMirror-scroll {
151 | overflow: scroll !important;
152 | margin-bottom: -30px;
153 | margin-right: -30px;
154 | padding-bottom: 30px;
155 | height: 100%;
156 | outline: none;
157 | position: relative;
158 | }
159 | .CodeMirror-sizer {
160 | position: relative;
161 | border-right: 30px solid transparent;
162 | }
163 |
164 | /* The fake, visible scrollbars. Used to force redraw during scrolling
165 | before actuall scrolling happens, thus preventing shaking and
166 | flickering artifacts. */
167 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
168 | position: absolute;
169 | z-index: 6;
170 | display: none;
171 | }
172 | .CodeMirror-vscrollbar {
173 | right: 0; top: 0;
174 | overflow-x: hidden;
175 | overflow-y: scroll;
176 | }
177 | .CodeMirror-hscrollbar {
178 | bottom: 0; left: 0;
179 | overflow-y: hidden;
180 | overflow-x: scroll;
181 | }
182 | .CodeMirror-scrollbar-filler {
183 | right: 0; bottom: 0;
184 | }
185 | .CodeMirror-gutter-filler {
186 | left: 0; bottom: 0;
187 | }
188 |
189 | .CodeMirror-gutters {
190 | position: absolute; left: 0; top: 0;
191 | z-index: 3;
192 | }
193 | .CodeMirror-gutter {
194 | white-space: normal;
195 | height: 100%;
196 | display: inline-block;
197 | margin-bottom: -30px;
198 | /* Hack to make IE7 behave */
199 | *zoom:1;
200 | *display:inline;
201 | }
202 | .CodeMirror-gutter-wrapper {
203 | position: absolute;
204 | z-index: 4;
205 | height: 100%;
206 | }
207 | .CodeMirror-gutter-elt {
208 | position: absolute;
209 | cursor: default;
210 | z-index: 4;
211 | }
212 | .CodeMirror-gutter-wrapper {
213 | -webkit-user-select: none;
214 | -moz-user-select: none;
215 | user-select: none;
216 | }
217 |
218 | .CodeMirror-lines {
219 | position: relative;
220 | cursor: text;
221 | min-height: 1px; /* prevents collapsing before first draw */
222 | overflow: hidden;
223 | &:after {
224 | content: " ";
225 | width: 1px;
226 | height: 100%;
227 | border-left: 1px solid #f6f6f6;
228 | position: absolute;
229 | top: 0;
230 | left: 682.125px;
231 | }
232 | }
233 | .CodeMirror pre {
234 | /* Reset some styles that the rest of the page might have set */
235 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
236 | border-width: 0;
237 | background: transparent;
238 | font-family: inherit;
239 | font-size: inherit;
240 | margin: 0;
241 | white-space: pre;
242 | word-wrap: normal;
243 | line-height: inherit;
244 | color: inherit;
245 | z-index: 2;
246 | position: relative;
247 | overflow: visible;
248 | -webkit-tap-highlight-color: transparent;
249 | }
250 | .CodeMirror-wrap pre {
251 | word-wrap: break-word;
252 | white-space: pre-wrap;
253 | word-break: normal;
254 | }
255 |
256 | .CodeMirror-linebackground {
257 | position: absolute;
258 | left: 0; right: 0; top: 0; bottom: 0;
259 | z-index: 0;
260 | }
261 |
262 | .CodeMirror-linewidget {
263 | position: relative;
264 | z-index: 2;
265 | overflow: auto;
266 | }
267 |
268 | .CodeMirror-widget {}
269 |
270 | .CodeMirror-code {
271 | outline: none;
272 | }
273 |
274 | /* Force content-box sizing for the elements where we expect it */
275 | .CodeMirror-scroll,
276 | .CodeMirror-sizer,
277 | .CodeMirror-gutter,
278 | .CodeMirror-gutters,
279 | .CodeMirror-linenumber {
280 | -moz-box-sizing: content-box;
281 | box-sizing: content-box;
282 | }
283 |
284 | .CodeMirror-measure {
285 | position: absolute;
286 | width: 100%;
287 | height: 0;
288 | overflow: hidden;
289 | visibility: hidden;
290 | }
291 | .CodeMirror-measure pre { position: static; }
292 |
293 | .CodeMirror div.CodeMirror-cursor {
294 | position: absolute;
295 | border-right: none;
296 | width: 0;
297 | }
298 |
299 | div.CodeMirror-cursors {
300 | visibility: hidden;
301 | position: relative;
302 | z-index: 3;
303 | }
304 | .CodeMirror-focused div.CodeMirror-cursors {
305 | visibility: visible;
306 | }
307 |
308 | .CodeMirror-selected { background: #b1d7fd; }
309 | .CodeMirror-focused .CodeMirror-selected { background: #b1d7fd; }
310 | .CodeMirror-crosshair { cursor: crosshair; }
311 | .CodeMirror ::selection { background: #b1d7fd; }
312 | .CodeMirror ::-moz-selection { background: #b1d7fd; }
313 |
314 | .cm-searching {
315 | background: #ffa;
316 | background: rgba(255, 255, 0, .4);
317 | }
318 |
319 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */
320 | .CodeMirror span { *vertical-align: text-bottom; }
321 |
322 | /* Used to force a border model for a node */
323 | .cm-force-border { padding-right: .1px; }
324 |
325 | .cm-header {
326 | color: #333;
327 | font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
328 | }
329 | .cm-header-1 {
330 | font-size: 2.25em;
331 | }
332 | .cm-header-2 { font-size: 1.75em; }
333 | .cm-header-3 { font-size: 1.5em; }
334 | .cm-header-4 { font-size: 1.25em; }
335 | .cm-header-5 { font-size: 1em; }
336 | .cm-header-6 { font-size: 1em; }
337 | .cm-strong { font-weight: bold; }
338 | .cm-toc {
339 | font-size: 140%;
340 | color: #aaa;
341 | background: #f0f0f0;
342 | }
343 | .cm-privately {
344 | color: #999;
345 | }
346 |
347 | @media print {
348 | /* Hide the cursor when printing */
349 | .CodeMirror div.CodeMirror-cursors {
350 | visibility: hidden;
351 | }
352 | }
353 |
354 | /* See issue #2901 */
355 | .cm-tab-wrap-hack:after { content: ''; }
356 |
357 | /* Help users use markselection to safely style text background */
358 | span.CodeMirror-selectedtext { background: none; }
359 |
360 | /* Tomorrow Comment */
361 | .hljs-comment {
362 | color: #8e908c;
363 | }
364 |
365 | /* Tomorrow Red */
366 | .hljs-variable,
367 | .hljs-attribute,
368 | .hljs-tag,
369 | .hljs-regexp,
370 | .ruby .hljs-constant,
371 | .xml .hljs-tag .hljs-title,
372 | .xml .hljs-pi,
373 | .xml .hljs-doctype,
374 | .html .hljs-doctype,
375 | .css .hljs-id,
376 | .css .hljs-class,
377 | .css .hljs-pseudo {
378 | color: #c82829;
379 | }
380 |
381 | /* Tomorrow Orange */
382 | .hljs-number,
383 | .hljs-preprocessor,
384 | .hljs-pragma,
385 | .hljs-built_in,
386 | .hljs-literal,
387 | .hljs-params,
388 | .hljs-constant {
389 | color: #f5871f;
390 | }
391 |
392 | /* Tomorrow Yellow */
393 | .ruby .hljs-class .hljs-title,
394 | .css .hljs-rule .hljs-attribute {
395 | color: #eab700;
396 | }
397 |
398 | /* Tomorrow Green */
399 | .hljs-string,
400 | .hljs-value,
401 | .hljs-inheritance,
402 | .hljs-header,
403 | .hljs-name,
404 | .ruby .hljs-symbol,
405 | .xml .hljs-cdata {
406 | color: #718c00;
407 | }
408 |
409 | /* Tomorrow Aqua */
410 | .hljs-title,
411 | .css .hljs-hexcolor {
412 | color: #3e999f;
413 | }
414 |
415 | /* Tomorrow Blue */
416 | .hljs-function,
417 | .python .hljs-decorator,
418 | .python .hljs-title,
419 | .ruby .hljs-function .hljs-title,
420 | .ruby .hljs-title .hljs-keyword,
421 | .perl .hljs-sub,
422 | .javascript .hljs-title,
423 | .coffeescript .hljs-title {
424 | color: #4271ae;
425 | }
426 |
427 | /* Tomorrow Purple */
428 | .hljs-keyword,
429 | .javascript .hljs-function {
430 | color: #8959a8;
431 | }
432 |
433 | .hljs {
434 | display: block;
435 | overflow-x: auto;
436 | background: white;
437 | color: #4d4d4c;
438 | padding: 0.5em;
439 | -webkit-text-size-adjust: none;
440 | }
441 |
442 | .coffeescript .javascript,
443 | .javascript .xml,
444 | .tex .hljs-formula,
445 | .xml .javascript,
446 | .xml .vbscript,
447 | .xml .css,
448 | .xml .hljs-cdata {
449 | opacity: 0.5;
450 | }
--------------------------------------------------------------------------------
/src/styles/editIcon.less:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'editfont';
3 | src: url('//at.alicdn.com/t/font_1450164737_488641.eot'); /* IE9*/
4 | src: url('//at.alicdn.com/t/font_1450164737_488641.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
5 | url('//at.alicdn.com/t/font_1450164737_488641.woff') format('woff'), /* chrome、firefox */
6 | url('//at.alicdn.com/t/font_1450164737_488641.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
7 | url('//at.alicdn.com/t/font_1450164737_488641.svg#iconfont') format('svg'); /* iOS 4.1- */
8 | }
9 |
10 | .edit-icon {
11 | position: relative;
12 | display: inline-block;
13 | font-style: normal;
14 | vertical-align: baseline;
15 | text-align: center;
16 | text-transform: none;
17 | text-rendering: auto;
18 | line-height: 1;
19 |
20 | &:before {
21 | display: block;
22 | font-family: "editfont" !important;
23 | }
24 | }
25 |
26 | .edit-icon-bold:before {content:"\e600";}
27 | .edit-icon-link:before {content:"\e601";}
28 | .edit-icon-code:before {content:"\e602";}
29 | .edit-icon-eyes:before {content:"\e603";}
30 | .edit-icon-eyes-slash:before {content:"\e604";}
31 | .edit-icon-h1:before {content:"\e605";}
32 | .edit-icon-h2:before {content:"\e605"; transform: scale(0.85)}
33 | .edit-icon-h3:before {content:"\e605"; transform: scale(0.70)}
34 | .edit-icon-keyboard:before {content:"\e606";}
35 | .edit-icon-image:before {content:"\e607";}
36 | .edit-icon-italic:before {content:"\e608";}
37 | .edit-icon-oList:before {content:"\e609";}
38 | .edit-icon-uList:before {content:"\e60a";}
39 | .edit-icon-redo:before {content:"\e60b"; transform: rotate(30deg)}
40 | .edit-icon-undo:before {content:"\e60c"; transform: rotate(-30deg)}
41 | .edit-icon-quote:before {content:"\e60d";}
42 | .edit-icon-del:before {content:"\e60e";}
--------------------------------------------------------------------------------
/src/styles/leaf.less:
--------------------------------------------------------------------------------
1 | @border-color: #e2e7ec;
2 | @font-color: #cabed3;
3 |
4 | @keyframes loadingCircle {
5 | 0% {
6 | transform-origin: 50% 50%;
7 | transform: rotate(0deg);
8 | }
9 | 100% {
10 | transform-origin: 50% 50%;
11 | transform: rotate(360deg);
12 | }
13 | }
14 |
15 |
16 | #leaf { height: 100%; }
17 | .leaf {
18 | width: 100%;
19 | height: 100%;
20 | min-height: ~"calc(100vh)";
21 | background: #f7f8f9;
22 |
23 | &-login {
24 | position: relative;
25 | width: 100%;
26 | height: 100%;
27 | padding: 50px 0 50px 0;
28 |
29 | &-link {
30 | width: 100%;
31 | height: auto;
32 | padding: 24px 0;
33 | margin: 0 auto;
34 | text-align: center;
35 |
36 | .dot {
37 | width: 12px;
38 | height: 12px;
39 | margin: 0 auto 24px auto;
40 | background: #f0f0f0;
41 | border: 1px solid @border-color;
42 | border-radius: 12px 12px;
43 | }
44 | }
45 |
46 | &-foot {
47 | padding: 60px 32px 32px 32px;
48 | text-align: center;
49 |
50 | a {
51 | color: #444;
52 | &:hover {
53 | text-decoration: underline;
54 | }
55 | }
56 | }
57 | &-contain {
58 | margin: 0 auto;
59 | width: 420px;
60 | height: 540px;
61 | border: 1px solid @border-color;
62 | background: #fff;
63 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
64 |
65 | &-head {
66 | padding: 48px 16px 24px 16px;
67 | height: auto;
68 |
69 | .head-logo {
70 | margin: 0 auto;
71 | background-repeat: no-repeat;
72 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFODA2MDNDRkI2QTcxMUU1QTUxMTgzRkM3MTQxRDFCOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFODA2MDNEMEI2QTcxMUU1QTUxMTgzRkM3MTQxRDFCOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkU4MDYwM0NEQjZBNzExRTVBNTExODNGQzcxNDFEMUI4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkU4MDYwM0NFQjZBNzExRTVBNTExODNGQzcxNDFEMUI4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+y70uDwAAGf5JREFUeNrsXQeY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);
73 | width: 64px;
74 | height: 64px;
75 | background-size: 64px 64px;
76 | }
77 |
78 | .head-logo-loading {
79 | -webkit-animation: loadingCircle .4s infinite linear;
80 | animation: loadingCircle .4s infinite linear;
81 | }
82 |
83 | .head-title {
84 | font-size: 12px;
85 | line-height: 32px;
86 | color: #444;
87 | text-align: center;
88 | font-family: "Courier New", Courier, monospace;
89 | }
90 | }
91 |
92 |
93 |
94 | .head-user {
95 | .avatar {
96 | display: block;
97 | width: 64px;
98 | height: 64px;
99 | margin: 0 auto;
100 | border-radius: 64px 64px;
101 | }
102 | .name {
103 | font-size: 14px;
104 | line-height: 32px;
105 | text-align: center;
106 | }
107 | }
108 |
109 | &-contain {
110 | width: 280px;
111 | margin: 24px auto;
112 | border-top: 1px solid @border-color;
113 | padding: 24px 0px;
114 |
115 | p {
116 | margin-bottom: 1.2em;
117 | }
118 |
119 | code {
120 | padding: 1px 4px;
121 | background: #f0f0f0;
122 | border-radius: 3px 3px;
123 | border: 1px solid @border-color;
124 | }
125 |
126 | a:hover {
127 | text-decoration: underline;
128 | }
129 |
130 | .text-right {
131 | text-align: right;
132 | }
133 | }
134 |
135 | &-foot {
136 | width: 280px;
137 | margin: 0 auto;
138 |
139 | p {
140 | padding: 0 0 8px 0;
141 | }
142 | }
143 |
144 | &-form {
145 | margin: 0 auto;
146 | width: 320px;
147 | padding: 8px 24px 48px 24px;
148 |
149 | .ant-btn {
150 | width: 100%;
151 | display: block;
152 | }
153 | }
154 | }
155 | }
156 |
157 | &-desktop {
158 | position: relative;
159 | width: 100%;
160 | height: 100%;
161 | min-height: 100vh;
162 | overflow: hidden;
163 |
164 | &-aside {
165 | position: absolute;
166 | top: 0;
167 | left: 0;
168 | width: 300px;
169 | background: #fff;
170 | height: 100%;
171 | border-right: 1px solid darken(@border-color, 3%);
172 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.1);
173 |
174 | .head {
175 | position: relative;
176 | width: 100%;
177 | height: auto;
178 | padding: 32px 0;
179 |
180 | &-logo {
181 | margin: 0 auto;
182 | background-repeat: no-repeat;
183 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFODA2MDNDRkI2QTcxMUU1QTUxMTgzRkM3MTQxRDFCOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFODA2MDNEMEI2QTcxMUU1QTUxMTgzRkM3MTQxRDFCOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkU4MDYwM0NEQjZBNzExRTVBNTExODNGQzcxNDFEMUI4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkU4MDYwM0NFQjZBNzExRTVBNTExODNGQzcxNDFEMUI4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+y70uDwAAGf5JREFUeNrsXQeY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);
184 | width: 48px;
185 | height: 48px;
186 | background-size: 48px 48px;
187 | }
188 |
189 | &-logo-loading {
190 | -webkit-animation: loadingCircle .5s infinite linear;
191 | animation: loadingCircle .5s infinite linear;
192 | }
193 |
194 | &-title {
195 | font-size: 12px;
196 | line-height: 32px;
197 | color: #444;
198 | text-align: center;
199 | font-family: "Courier New", Courier, monospace;
200 | }
201 | }
202 |
203 | .body {
204 | &-project {
205 | &-title {
206 | padding: 0px 32px 8px 32px;
207 | display: block;
208 | line-height: 24px;
209 | color: #666;
210 | font-size: 16px;
211 | font-weight: normal;
212 | text-align: center;
213 |
214 | a {
215 | color: #666;
216 | }
217 | }
218 |
219 | &-description {
220 | padding: 0 32px;
221 | font-size: 12px;
222 | color: #999;
223 | display: none;
224 | }
225 |
226 | &-card {
227 | padding: 16px 32px;
228 | display: none;
229 |
230 | &-item {
231 | padding: 4px 8px;
232 | border-radius: 24px 24px;
233 | margin-right: 12px;
234 | color: #fff;
235 | background: @primary-color;
236 | }
237 | }
238 |
239 | }
240 |
241 | &-menu {
242 | padding: 24px 0;
243 |
244 | &-item {
245 | margin-bottom: 24px;
246 |
247 | .link {
248 | position: relative;
249 | width: 48px;
250 | height: 48px;
251 | margin: 0 auto;
252 | line-height: 48px;
253 | display: block;
254 | border-radius: 24px 24px;
255 | background: #fff;
256 | border: 1px solid @border-color;
257 | text-align: center;
258 | color: #999;
259 | cursor: pointer;
260 |
261 | .icon {
262 | font-size: 16px;
263 | }
264 |
265 | .count {
266 | position: absolute;
267 | top: -8px;
268 | right: -8px;
269 | height: 24px;
270 | line-height: 24px;
271 | width: 24px;
272 | font-size: 12px;
273 | border-radius: 12px 12px;
274 | display: block;
275 | border: 1px solid @border-color;
276 | color: #666;
277 | background: #fff;
278 | opacity: 0;
279 | visibility: hidden;
280 | transform: scale(0);
281 | transition: .3s all ease-in-out;
282 |
283 | &-active {
284 | opacity: 1;
285 | visibility: visible;
286 | transform: scale(1);
287 | }
288 | }
289 |
290 | &:hover {
291 | border: 1px solid darken(@border-color, 15%);
292 | }
293 |
294 | &-active {
295 | color: #444;
296 | border: 1px solid darken(@border-color, 15%);
297 | background: #f7f8f9;
298 | }
299 | }
300 |
301 | .text {
302 | opacity: 0;
303 | visibility: hidden;
304 | transition: all .3s ease-in-out;
305 | transform: translateY(6px);
306 | text-align: center;
307 | line-height: 32px;
308 | height: 32px;
309 | color: #999;
310 | font-size: 12px;
311 | }
312 |
313 | &:hover {
314 | .text {
315 | opacity: 1;
316 | visibility: visible;
317 | transform: translateY(0px);
318 | }
319 | }
320 |
321 | }
322 | }
323 | }
324 |
325 | .foot {
326 | position: absolute;
327 | bottom: 0px;
328 | left: 0;
329 | width: 100%;
330 | background: #fff;
331 |
332 | .foot-user {
333 | position: relative;
334 | padding: 8px 24px;
335 | cursor: pointer;
336 | border-top: 1px solid @border-color;
337 | background: #f7f8f9;
338 |
339 | &-avatar {
340 | width: 32px;
341 | height: 32px;
342 | border-radius: 32px;
343 | display: inline-block;
344 | }
345 |
346 | &-info {
347 | display: inline-block;
348 |
349 | .name {
350 | font-size: 14px;
351 | line-height: 20px;
352 |
353 | .anticon {
354 | font-size: 12px;
355 | color: #999;
356 | }
357 | }
358 | .email {
359 | font-size: 12px;
360 | line-height: 16px;
361 | color: #999;
362 | }
363 | }
364 |
365 | &-actions {
366 | position: absolute;
367 | width: ~"calc(100% - 64px)";
368 | height: auto;
369 | padding: 8px 0;
370 | background: #fff;
371 | border: 1px solid @border-color;
372 | border-radius: 3px 3px;
373 | bottom: 46px;
374 | left: 32px;
375 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
376 | opacity: 0;
377 | visibility: hidden;
378 | transition: all .3s ease-in-out;
379 | transform: translateY(10px);
380 |
381 | .item {
382 | line-height: 32px;
383 |
384 | a {
385 | line-height: 32px;
386 | padding: 0 32px;
387 | color: #444;
388 | display: block;
389 |
390 | .anticon {
391 | color: #999;
392 | padding-right: 8px;
393 | }
394 |
395 | &:hover {
396 | color: #444;
397 | background: #f7f8f9;
398 | }
399 | }
400 | }
401 | }
402 |
403 | &:hover {
404 | background: #f7f8f9;
405 | .foot-user-actions {
406 | opacity: 1;
407 | visibility: visible;
408 | transform: translateY(0);
409 | }
410 | }
411 | }
412 |
413 | .foot-copyright {
414 | padding: 8px 32px;
415 | text-align: center;
416 | border-top: 1px solid @border-color;
417 | display: inline-block;
418 |
419 | a {
420 | color: #666;
421 | text-decoration: underline;
422 | }
423 |
424 |
425 | }
426 | }
427 | }
428 |
429 |
430 | &-main {
431 | width: 100%;
432 | height: 100%;
433 | min-height: 100vh;
434 | padding-left: 300px;
435 | overflow: hidden;
436 |
437 | &-wrap {
438 | height: 100%;
439 | padding: 0 48px;
440 | overflow-x: hidden;
441 | overflow-y: auto;
442 |
443 |
444 | }
445 | }
446 |
447 | &-head {
448 | width: 100%;
449 | padding: 32px 0;
450 | height: 112px;
451 | border-bottom: 1px solid @border-color;
452 |
453 | .title {
454 | line-height: 48px;
455 | font-size: 16px;
456 | color: #777;
457 | font-weight: normal;
458 |
459 | .anticon {
460 | margin: 0 5px;
461 | font-size: 12px;
462 | transform: scale(0.8);
463 | color: darken(@border-color, 15%);
464 | }
465 | }
466 |
467 | .action {
468 | padding: 8px 8px;
469 |
470 | .anticon {
471 | color: #999;
472 | }
473 | .ant-btn {
474 | float: right;
475 | }
476 | }
477 | }
478 |
479 | &-list {
480 | padding: 48px 0 24px 0;
481 |
482 | &-loading {
483 | width: 64px;
484 | height: 64px;
485 | margin: 100px auto;
486 | text-align: center;
487 | font-size: 24px;
488 | }
489 |
490 | &-cnt {
491 | .file {
492 | display: block;
493 | float: left;
494 | width: 124px;
495 | height: 148px;
496 | margin-bottom: 24px;
497 | margin-right: 16px;
498 | color: #666;
499 | border: 1px solid transparent;
500 | transition: all .3s ease-in-out;
501 |
502 | &-card {
503 | width: 100%;
504 | height: 100%;
505 | display: block;
506 | padding: 16px 8px;
507 |
508 | &-type {
509 | margin: 0 auto;
510 | width: 42px;
511 | height: 51px;
512 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVMAAAC/CAYAAABDuj85AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo1OUQ1QzQzNUEyMjA2ODExODA4Mzg0MDM5MjE2QURGNSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4MTMwOTJDNkI2NkIxMUU1QTUxMTgzRkM3MTQxRDFCOCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4MTMwOTJDNUI2NkIxMUU1QTUxMTgzRkM3MTQxRDFCOCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NTlENUM0MzVBMjIwNjgxMTgwODM4NDAzOTIxNkFERjUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NTlENUM0MzVBMjIwNjgxMTgwODM4NDAzOTIxNkFERjUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6QvSK4AAAEp0lEQVR42uzdP2iUdxjA8fdqWhNNq0jroFCHiDhYKrq0CAGhcSpFRQrq4qCQSQdRh9JODg6CuGSSQsUsBRVFREgKNQaNtIZCZzelQ0f/JChyfV7uQE7QGn73Xi6/+3zgGXQwyZPc1/e9y71vrV6vFwCk+cAKAMQUQEwBxBQAMQUQUwAxBRBTAMQUQEwBxBRATAEQUwAxBVgC+tr9D05MzbimX+dNxox04yc2MvyV7w6OTFkyvom5ag0gpqTbHTNuDZDJab5TvM568vTZw5nZv4eafzwQ8zzmiM2AI1MW4OPBlUNv/NXhmPM2A2JKuqMxZ6wBxJR0p2J+sAYQU9KdjjluDSCmpDsbM2oNIKakG4s5ZA0gpqSpxfwcs98qQExJD+qlmO+sAsSU9O93+bbTXVYBYkr69/xmzLBVgJiSZlnMbzHe7wtiSqLyugy3Y7ZZBYgpaT6KuRuzxSpATEmzPOZ+zCarADElzYqYBzFDVgFiSprBZlA3WAUsXF+3f4ITUzPZLr8LL569KmY25suYRx4ekFFMXa2/49Y0j1C3xvxjHZBJTFkUa2P+bAb136X8heR8ZkN3Hbx5zpS3WdcM6qcZfC31Lp+XMdMxX/uxW7rElHf5PKY8tFttFZWfIe4oGm+iOGAdYkqeyl+Xulc0XpyiWh/GXIzZZxViSp42x9wpGr8+RbXK6yaMO+UXU/L1RczvReMX/KlW+TbfazEbrUJMydP2mMmYAauo3GdF49qznl4RUzJVnn7eah49Ua3yAjTXnQ2IKfkqLyx9Q1A7uutPrEJMydNIzOWi8YIJ1dpZNN7muyem3zq6k3dAkeLbmF9jvo95ZR2VKn9F7Yo1VKrmyJTFtDfmF0eoOM2HdAdjLqT+zw5iCkVxKGZMUBFTSDcac84a6EVegKLdjsW8iDnZqQ/oMntU/bP0PpfmE1OqcCJmPuanDn7MurVT4c/Q/z595bYliyjzuwj82OGYgtP8Hg5OVjuam5t/PP3HX+t9R+hFXoCibQYG+oUUR6bgSB1eW+hTjI5MAZzmA4gpgJgCIKYAYgogpgBiCoCYAogpgJgCiCkAYgogpgBiCtAj3LaEFq4FCpnG1IMbcJoPIKYAiCmAmAKIKYCYAiCmAGIKIKYAYgqAmAKIKYCYAiCmAGIKIKYAYgqAmAK0m3tA0cJtYiDTmHpwA07zAcQUADEFEFMAMQUQUwDEFEBMAcQUQEwBEFMAMQUQUwDEFEBMAcQUQEwBaOG2JbRwZwPINKYe3IDTfAAxBUBMAcQUQEwBxBQAMQUQUwAxBRBTAMQUQEwBxBQAMQUQUwAxBRBTAMQUoN3cA4oWbhMDmcbUgxtwmg8gpgCIKYCYAogpgJgCIKYAYgogpgBiCoCYAogpgJgCIKYAYgogpgBiCkALty2hhTsbQKYx9eAGxBSczdAjPGcKIKYATvOhnWpWgJhCAi9S4jQfQEwBEFMAMQUQUwAxBUBMAcQUQEwBxBQAMQWoWKXvzXddScCRKQBiCtBJtXq9bgsAjkwBxBRATAEQUwAxBRBTADEFQEwBxBRATAHEFAAxBRBTADEFEFMAxBRATAHEFKCn/CfAAEoXpyRx2/qwAAAAAElFTkSuQmCC);
513 | background-repeat: no-repeat;
514 | background-size: 110px 61px;
515 | }
516 |
517 | &-name {
518 | margin-top: 16px;
519 | font-size: 12px;
520 | color: #666;
521 | text-align: center;
522 | min-height: 42px;
523 | max-height: 63px;
524 | overflow: hidden;
525 | }
526 | }
527 |
528 | &:hover {
529 | background: #fff;
530 | border: 1px solid darken(@border-color, 3%);
531 | box-shadow: 0 2px 15px rgba(0,0,0,0.1);
532 | transform: translate(0, -4px);
533 | }
534 | }
535 |
536 | .file-f {
537 | .file-card {
538 | .file-card-type {
539 | width: 42px;
540 | height: 51px;
541 | background-position: -7px -4px;
542 | }
543 | }
544 |
545 | }
546 |
547 | .file-d {
548 | .file-card {
549 | .file-card-type {
550 | width: 51px;
551 | height: 51px;
552 | background-position: -56px 0px;
553 | }
554 | }
555 | }
556 | }
557 | }
558 |
559 | &-foot {
560 | padding: 24px 0;
561 | text-align: center;
562 | font-size: 12px;
563 | color: #666;
564 | }
565 |
566 | }
567 |
568 | &-post {
569 | width: 100%;
570 | height: 100%;
571 |
572 | &-head {
573 | position: relative;
574 | width: 100%;
575 | height: 48px;
576 | padding: 0 50px;
577 | background: #fff;
578 | border-bottom: 1px solid @border-color;
579 | z-index: 102;
580 |
581 | .back {
582 | display: inline-block;
583 | height: 48px;
584 | line-height: 48px;
585 | width: 48px;
586 | color: #999;
587 | }
588 |
589 | .title {
590 | font-size: 14px;
591 | color: #444;
592 | line-height: 48px;
593 | min-width: 300px;
594 | text-align: center;
595 | font-weight: normal;
596 | }
597 |
598 | .meta {
599 | position: relative;
600 | width: 100%;
601 |
602 | &-title {
603 | position: relative;
604 | margin: 0 auto;
605 | line-height: 48px;
606 | font-size: 14px;
607 | color: #444;
608 | text-align: center;
609 | font-weight: normal;
610 | cursor: pointer;
611 |
612 | .anticon {
613 | font-size: 12px;
614 | color: #ccc;
615 | margin-left: 6px;
616 | transform: scale(0.6);
617 | transition: all .3s ease-in-out;
618 | }
619 | }
620 |
621 | &-card {
622 | position: absolute;
623 | top: 40px;
624 | left: 50%;
625 | margin-left: -180px;
626 | width: 360px;
627 | height: auto;
628 | padding: 16px 0 0 0;
629 | background: #fff;
630 | border: 1px solid @border-color;
631 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
632 | opacity: 0;
633 | visibility: hidden;
634 | transition: all .3s ease-in-out;
635 | transform: translateY(-10px);
636 |
637 | &-item {
638 | padding: 0 16px;
639 | margin-bottom: 8px;
640 | .name {
641 | color: #999;
642 | padding-bottom: 8px;
643 | }
644 | .cnt {
645 | color: #444;
646 | }
647 | }
648 |
649 | &-edit {
650 | display: block;
651 | cursor: pointer;
652 | padding: 8px 32px;
653 | line-height: 24px;
654 | text-align: center;
655 | background: #f7f8f9;
656 | border-top: 1px solid @border-color;
657 | border-radius: 0 0 3px 3px;
658 | margin-top: 16px;
659 | color: #666;
660 |
661 | .anticon {
662 | color: #ccc;
663 | }
664 |
665 | &:hover {
666 | color: #444;
667 | }
668 | }
669 | }
670 |
671 | &:hover {
672 | .meta-title {
673 | .anticon {
674 | color: #666;
675 | }
676 | }
677 |
678 | .meta-card {
679 | opacity: 1;
680 | visibility: visible;
681 | transform: translateY(0px);
682 | }
683 | }
684 | }
685 |
686 | .action-list {
687 | position: relative;
688 | float: right;
689 |
690 | &-dropdown {
691 | position: absolute;
692 | top: 40px;
693 | right: 0;
694 | width: 120px;
695 | height: auto;
696 | padding: 8px 0;
697 | background: #fff;
698 | border: 1px solid @border-color;
699 | border-radius: 3px 3px;
700 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
701 | opacity: 0;
702 | visibility: hidden;
703 | transition: all .3s ease-in-out;
704 | transform: translateY(-10px);
705 |
706 | .item {
707 | line-height: 32px;
708 |
709 | a {
710 | line-height: 32px;
711 | padding: 0 16px;
712 | color: #666;
713 | display: block;
714 |
715 | .anticon {
716 | color: #999;
717 | padding-right: 8px;
718 | }
719 |
720 | &:hover {
721 | color: #444;
722 | background: #f7f8f9;
723 | }
724 | }
725 | }
726 | }
727 |
728 | &-toggle {
729 | width: 48px;
730 | height: 48px;
731 | display: inline-block;
732 | text-align: center;
733 | line-height: 48px;
734 | cursor: pointer;
735 | }
736 |
737 | &:hover {
738 | .action-list-dropdown {
739 | opacity: 1;
740 | visibility: visible;
741 | transform: translateY(0px);
742 | }
743 | }
744 |
745 | }
746 | }
747 |
748 | }
749 |
750 | .leaf-editor {
751 | position: absolute;
752 | width: 100%;
753 | height: ~"calc(100vh - 48px)";
754 | overflow: hidden;
755 |
756 | .leaf-editor-tool {
757 | position: relative;
758 | height: 50px;
759 | padding: 6px 0;
760 | width: 100%;
761 | z-index: 100;
762 | transition: all .3s ease-in-out;
763 | border-bottom: 1px solid transparent;
764 |
765 | .leaf-editor-tool-list {
766 | margin: 0 auto;
767 | width: 816px;
768 | height: 38px;
769 | padding: 6px 0;
770 | }
771 | .leaf-editor-tool-separator {
772 | border-left: 1px solid @border-color;
773 | width: 0px;
774 | height: 18px;
775 | margin: 5px 10px;
776 | }
777 | .leaf-editor-tool-icon {
778 | border: 1px solid transparent;
779 | display: inline-block;
780 | height: 26px;
781 | width: 30px;
782 | text-align: center;
783 | background: transparent;
784 | border-radius: 2px 2px;
785 | font-size: 12px;
786 | margin: 0 1px;
787 |
788 | &-active {
789 | background: rgba(255, 255, 255, 0.48);
790 | }
791 |
792 | &-active,
793 | &:hover {
794 | border: 1px solid rgba(0, 0, 0, 0.12);
795 | color: #444;
796 | }
797 |
798 | &:focus {
799 | outline: none;
800 | }
801 |
802 | &-disabled {
803 | color: #a8a8a8;
804 |
805 | &:hover {
806 | border: 1px solid transparent;
807 | color: #a8a8a8;
808 | cursor: not-allowed;
809 | }
810 | }
811 | }
812 |
813 | &-active {
814 | border-bottom: 1px solid @border-color;
815 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
816 | }
817 |
818 | }
819 |
820 | .leaf-editor-wrap {
821 | width: 100%;
822 | height: ~"calc(100vh - 96px)";
823 | overflow-x: hidden;
824 | overflow-y: auto;
825 | padding-top: 6px;
826 | padding: 0 50px;
827 |
828 | .leaf-editor-container {
829 | position: relative;
830 | width: 816px;
831 | margin: 0 auto;
832 | min-height: 960px;
833 | height: auto;
834 | background: #fff;
835 | border: 1px solid @border-color;
836 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.1);
837 |
838 | .loading {
839 | position: absolute;
840 | left: 90px;
841 | top: 90px;
842 | width: 634px;
843 | background: #fff;
844 | height: 500px;
845 | padding: 100px;
846 | display: block;
847 | text-align: center;
848 | font-size: 24px;
849 | z-index: 100;
850 | }
851 |
852 | .title {
853 | width: 100%;
854 | font-size: 30px;
855 | height: 60px;
856 | margin-bottom: 16px;
857 | border-bottom: 1px solid #e8ecf1;
858 |
859 | .title-edit {
860 | outline: none;
861 | border: none;
862 | box-shadow: none;
863 | display: block;
864 | width: 100%;
865 | height: 50px;
866 | line-height: 50px;
867 | color: #333;
868 | font-weight: bold;
869 | font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
870 | }
871 | }
872 |
873 | .pen {
874 | display: block;
875 | position: relative;
876 | min-height: 960px;
877 | height: auto;
878 | width: 100%;
879 | padding: 90px 90px;
880 | transition: all .3s ease-in-out;
881 |
882 | .content {
883 | position: relative;
884 | min-height: 500px;
885 | height: auto;
886 | width: 100%;
887 | }
888 |
889 | &-hide {
890 | display: none;
891 | }
892 | }
893 |
894 | .view {
895 | position: relative;
896 | width: 100%;
897 | min-height: 960px;
898 | height: auto;
899 | padding: 90px 90px;
900 | display: none;
901 | transition: all .3s ease-in-out;
902 | background: #fff;
903 | }
904 |
905 | .view-show {
906 | display: block;
907 | }
908 | }
909 |
910 | .leaf-editor-footer {
911 | padding: 30px 0px;
912 | text-align: center;
913 | line-height: 30px;
914 | font-size: 14px;
915 | height: 90px;
916 |
917 | a {
918 | color: #444;
919 | &:hover {
920 | text-decoration: underline;
921 | }
922 | }
923 | }
924 | }
925 |
926 | &-github {
927 | color: #333;
928 | font-size: 14px;
929 | line-height: 1.6;
930 | word-wrap: break-word;
931 |
932 | a {
933 | color: #4078c0;
934 | text-decoration: none;
935 | background-color: transparent;
936 | }
937 |
938 | a:active,
939 | a:hover {
940 | outline: 0;
941 | }
942 |
943 | strong {
944 | font-weight: bold;
945 | }
946 |
947 | h1 {
948 | font-size: 2em;
949 | margin: 0.67em 0;
950 | }
951 |
952 | img {
953 | border: 0;
954 | }
955 |
956 | hr {
957 | box-sizing: content-box;
958 | height: 0;
959 | margin: 15px 0;
960 | overflow: hidden;
961 | background: transparent;
962 | border: 0;
963 | border-bottom: 1px solid #ddd;
964 | }
965 |
966 | pre {
967 | overflow: auto;
968 | }
969 |
970 | hr:before {
971 | display: table;
972 | content: "";
973 | }
974 |
975 | hr:after {
976 | display: table;
977 | clear: both;
978 | content: "";
979 | }
980 |
981 | code,
982 | kbd,
983 | pre {
984 | font-family: monospace, monospace;
985 | font-size: 1em;
986 | }
987 |
988 | input {
989 | color: inherit;
990 | font: inherit;
991 | margin: 0;
992 | }
993 |
994 | html input[disabled] {
995 | cursor: default;
996 | }
997 |
998 | input {
999 | line-height: normal;
1000 | font: 13px / 1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
1001 | }
1002 |
1003 | input[type="checkbox"] {
1004 | box-sizing: border-box;
1005 | padding: 0;
1006 | }
1007 |
1008 | table {
1009 | border-collapse: collapse;
1010 | border-spacing: 0;
1011 | }
1012 |
1013 | td,
1014 | th {
1015 | padding: 0;
1016 | }
1017 |
1018 | h1,
1019 | h2,
1020 | h3,
1021 | h4,
1022 | h5,
1023 | h6 {
1024 | margin-top: 15px;
1025 | margin-bottom: 15px;
1026 | line-height: 1.1;
1027 | }
1028 |
1029 | h1 {
1030 | font-size: 30px;
1031 | }
1032 |
1033 | h2 {
1034 | font-size: 21px;
1035 | }
1036 |
1037 | h3 {
1038 | font-size: 16px;
1039 | }
1040 |
1041 | h4 {
1042 | font-size: 14px;
1043 | }
1044 |
1045 | h5 {
1046 | font-size: 12px;
1047 | }
1048 |
1049 | h6 {
1050 | font-size: 11px;
1051 | }
1052 |
1053 | blockquote {
1054 | margin: 0;
1055 | }
1056 |
1057 | ul,
1058 | ol {
1059 | padding: 0;
1060 | margin-top: 0;
1061 | margin-bottom: 0;
1062 | }
1063 |
1064 | ol ol,
1065 | ul ol {
1066 | list-style-type: lower-roman;
1067 | }
1068 |
1069 | ul ul ol,
1070 | ul ol ol,
1071 | ol ul ol,
1072 | ol ol ol {
1073 | list-style-type: lower-alpha;
1074 | }
1075 |
1076 | dd {
1077 | margin-left: 0;
1078 | }
1079 |
1080 | code {
1081 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
1082 | font-size: 12px;
1083 | }
1084 |
1085 | pre {
1086 | margin-top: 0;
1087 | margin-bottom: 0;
1088 | font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
1089 | }
1090 |
1091 | .select::-ms-expand {
1092 | opacity: 0;
1093 | }
1094 |
1095 | .octicon {
1096 | font: normal normal normal 24px/1 Consolas, "Liberation Mono", Menlo, Courier, monospace;
1097 | display: inline-block;
1098 | font-weight: lighter;
1099 | }
1100 |
1101 | .octicon-link:before {
1102 | content: '#';
1103 | }
1104 |
1105 | >*:first-child {
1106 | margin-top: 0 !important;
1107 | }
1108 |
1109 | >*:last-child {
1110 | margin-bottom: 0 !important;
1111 | }
1112 |
1113 | a:not([href]) {
1114 | color: inherit;
1115 | text-decoration: none;
1116 | }
1117 |
1118 | .anchor {
1119 | display: inline-block;
1120 | padding-right: 2px;
1121 | margin-left: -18px;
1122 | }
1123 |
1124 | .anchor:focus {
1125 | outline: none;
1126 | }
1127 |
1128 | h1,
1129 | h2,
1130 | h3,
1131 | h4,
1132 | h5,
1133 | h6 {
1134 | margin-top: 1em;
1135 | margin-bottom: 16px;
1136 | font-weight: bold;
1137 | line-height: 1.4;
1138 | }
1139 |
1140 | h1 .octicon-link,
1141 | h2 .octicon-link,
1142 | h3 .octicon-link,
1143 | h4 .octicon-link,
1144 | h5 .octicon-link,
1145 | h6 .octicon-link {
1146 | color: #999;
1147 | vertical-align: middle;
1148 | visibility: hidden;
1149 | }
1150 |
1151 | h1:hover .anchor,
1152 | h2:hover .anchor,
1153 | h3:hover .anchor,
1154 | h4:hover .anchor,
1155 | h5:hover .anchor,
1156 | h6:hover .anchor {
1157 | text-decoration: none;
1158 | }
1159 |
1160 | h1:hover .anchor .octicon-link,
1161 | h2:hover .anchor .octicon-link,
1162 | h3:hover .anchor .octicon-link,
1163 | h4:hover .anchor .octicon-link,
1164 | h5:hover .anchor .octicon-link,
1165 | h6:hover .anchor .octicon-link {
1166 | visibility: visible;
1167 | }
1168 |
1169 | h1 {
1170 | padding-bottom: 0.3em;
1171 | font-size: 2.25em;
1172 | line-height: 1.2;
1173 | border-bottom: 1px solid #eee;
1174 | }
1175 |
1176 | h1 .anchor {
1177 | line-height: 1;
1178 | }
1179 |
1180 | h2 {
1181 | padding-bottom: 0.3em;
1182 | font-size: 1.75em;
1183 | line-height: 1.225;
1184 | border-bottom: 1px solid #eee;
1185 | }
1186 |
1187 | h2 .anchor {
1188 | line-height: 1;
1189 | }
1190 |
1191 | h3 {
1192 | font-size: 1.5em;
1193 | line-height: 1.43;
1194 | }
1195 |
1196 | h3 .anchor {
1197 | line-height: 1.2;
1198 | }
1199 |
1200 | h4 {
1201 | font-size: 1.25em;
1202 | }
1203 |
1204 | h4 .anchor {
1205 | line-height: 1.2;
1206 | }
1207 |
1208 | h5 {
1209 | font-size: 1em;
1210 | }
1211 |
1212 | h5 .anchor {
1213 | line-height: 1.1;
1214 | }
1215 |
1216 | h6 {
1217 | font-size: 1em;
1218 | color: #777;
1219 | }
1220 |
1221 | h6 .anchor {
1222 | line-height: 1.1;
1223 | }
1224 |
1225 | p,
1226 | blockquote,
1227 | ul,
1228 | ol,
1229 | dl,
1230 | table,
1231 | pre {
1232 | margin-top: 0;
1233 | margin-bottom: 16px;
1234 | }
1235 |
1236 | hr {
1237 | height: 4px;
1238 | padding: 0;
1239 | margin: 16px 0;
1240 | background-color: #e7e7e7;
1241 | border: 0 none;
1242 | }
1243 |
1244 | ul,
1245 | ol {
1246 | padding-left: 2em;
1247 | }
1248 |
1249 | ul {
1250 | list-style-type: disc;
1251 | }
1252 |
1253 | ol {
1254 | list-style-type: decimal;
1255 | }
1256 |
1257 | ul ul,
1258 | ul ol,
1259 | ol ol,
1260 | ol ul {
1261 | margin-top: 0;
1262 | margin-bottom: 0;
1263 | }
1264 |
1265 | li>p {
1266 | margin-top: 16px;
1267 | }
1268 |
1269 | dl {
1270 | padding: 0;
1271 | }
1272 |
1273 | dl dt {
1274 | padding: 0;
1275 | margin-top: 16px;
1276 | font-size: 1em;
1277 | font-style: italic;
1278 | font-weight: bold;
1279 | }
1280 |
1281 | dl dd {
1282 | padding: 0 16px;
1283 | margin-bottom: 16px;
1284 | }
1285 |
1286 | blockquote {
1287 | padding: 0 15px;
1288 | color: #777;
1289 | border-left: 4px solid #ddd;
1290 | }
1291 |
1292 | blockquote>:first-child {
1293 | margin-top: 0;
1294 | }
1295 |
1296 | blockquote>:last-child {
1297 | margin-bottom: 0;
1298 | }
1299 |
1300 | table {
1301 | display: block;
1302 | width: 100%;
1303 | overflow: auto;
1304 | word-break: normal;
1305 | word-break: keep-all;
1306 | }
1307 |
1308 | table th {
1309 | font-weight: bold;
1310 | }
1311 |
1312 | table th,
1313 | table td {
1314 | padding: 6px 13px;
1315 | border: 1px solid #ddd;
1316 | }
1317 |
1318 | table tr {
1319 | background-color: #fff;
1320 | border-top: 1px solid #ccc;
1321 | }
1322 |
1323 | table tr:nth-child(2n) {
1324 | background-color: #f8f8f8;
1325 | }
1326 |
1327 | img {
1328 | max-width: 100%;
1329 | box-sizing: content-box;
1330 | background-color: #fff;
1331 | }
1332 |
1333 | code {
1334 | position: relative;
1335 | top: -2px;
1336 | padding: 0.3em 7px;
1337 | margin: 0;
1338 | font-size: 85%;
1339 | background-color: rgba(0,0,0,0.04);
1340 | border-radius: 3px;
1341 | }
1342 |
1343 | pre > code {
1344 | padding: 0;
1345 | margin: 0;
1346 | background: transparent;
1347 | border: 0;
1348 | }
1349 |
1350 | .highlight {
1351 | margin-bottom: 16px;
1352 | }
1353 |
1354 | .highlight pre,
1355 | pre {
1356 | padding: 16px;
1357 | font-size: 85%;
1358 | line-height: 1.45;
1359 | background-color: #f7f7f7;
1360 | border-radius: 3px;
1361 | }
1362 |
1363 | .highlight pre {
1364 | margin-bottom: 0;
1365 | word-break: normal;
1366 | }
1367 |
1368 | pre code:before,
1369 | pre code:after {
1370 | content: normal;
1371 | }
1372 |
1373 | kbd {
1374 | display: inline-block;
1375 | padding: 3px 5px;
1376 | font-size: 11px;
1377 | line-height: 10px;
1378 | color: #555;
1379 | vertical-align: middle;
1380 | background-color: #fcfcfc;
1381 | border: solid 1px #ccc;
1382 | border-bottom-color: #bbb;
1383 | border-radius: 3px;
1384 | box-shadow: inset 0 -1px 0 #bbb;
1385 | }
1386 |
1387 | kbd {
1388 | display: inline-block;
1389 | padding: 3px 5px;
1390 | font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
1391 | line-height: 10px;
1392 | color: #555;
1393 | vertical-align: middle;
1394 | background-color: #fcfcfc;
1395 | border: solid 1px #ccc;
1396 | border-bottom-color: #bbb;
1397 | border-radius: 3px;
1398 | box-shadow: inset 0 -1px 0 #bbb;
1399 | }
1400 |
1401 | .task-list-item {
1402 | list-style-type: none;
1403 | }
1404 |
1405 | .task-list-item+.task-list-item {
1406 | margin-top: 3px;
1407 | }
1408 |
1409 | .task-list-item input {
1410 | margin: 0 0.35em 0.25em -1.6em;
1411 | vertical-align: middle;
1412 | }
1413 |
1414 | :checked+.radio-label {
1415 | z-index: 1;
1416 | position: relative;
1417 | border-color: #4078c0;
1418 | }
1419 |
1420 | }
1421 |
1422 | }
1423 |
1424 | .love {
1425 | position: relative;
1426 | width: 12px;
1427 | height: 10px;
1428 | display: inline-block;
1429 | text-indent: -9999em;
1430 | overflow: hidden;
1431 |
1432 | &:before,
1433 | &:after {
1434 | position: absolute;
1435 | content: "";
1436 | left: 6px;
1437 | top: 0;
1438 | width: 6px;
1439 | height: 10px;
1440 | background: #e22025;
1441 | border-radius: 6px 6px 0 0;
1442 | transform: rotate(-45deg);
1443 | transform-origin: 0 100%;
1444 | }
1445 |
1446 | &:after {
1447 | left: 0;
1448 | transform: rotate(45deg);
1449 | transform-origin :100% 100%;
1450 | }
1451 | }
1452 | }
--------------------------------------------------------------------------------
/src/utils/editorFormat.js:
--------------------------------------------------------------------------------
1 | const FORMATS = {
2 | h1: { type: 'block', token: 'header-1', before: '#', re: /^#\s+/, placeholder: '大标题' },
3 | h2: { type: 'block', token: 'header-2', before: '##', re: /^##\s+/, placeholder: '中标题' },
4 | h3: { type: 'block', token: 'header-3', before: '###', re: /^###\s+/, placeholder: '小标题' },
5 | bold: { type: 'inline', token: 'strong', before: '**', after: '**', placeholder: 'bold text' },
6 | italic: { type: 'inline', token: 'em', before: '_', after: '_', placeholder: 'italic text' },
7 | quote: { type: 'block', token: 'quote', re: /^\>\s+/, before: '>', placeholder: 'quote' },
8 | oList: { type: 'block', before: '1. ', re: /^\d+\.\s+/, placeholder: 'List' },
9 | uList: { type: 'block', before: '* ', re: /^[\*\-]\s+/, placeholder: 'List' },
10 | link: { type: 'inline', token: 'link', before: '[', after: ']()', placeholder: 'Link' },
11 | image: { type: 'inline', token: 'image', before: '![', after: ']()', placeholder: 'Image' },
12 | code: { type: 'inline', token: 'code', before: '`', after: '`', placeholder: 'code' },
13 | del: { type: 'inline', token: 'strikethrough', before: '~~', after: '~~', placeholder: 'del' },
14 | };
15 |
16 | const FORMAT_TOKENS = {};
17 | Object.keys(FORMATS).forEach(key => {
18 | if (FORMATS[key].token) FORMAT_TOKENS[FORMATS[key].token] = key;
19 | });
20 |
21 | export function getCursorState(cm, pos) {
22 | let currentPos;
23 | currentPos = pos || cm.getCursor('start');
24 | var cs = {
25 | render: false
26 | };
27 | var token = cs.token = cm.getTokenAt(currentPos);
28 | if (!token.type) return cs;
29 | var tokens = token.type.split(' ');
30 | tokens.forEach(t => {
31 | if (FORMAT_TOKENS[t]) {
32 | cs[FORMAT_TOKENS[t]] = true;
33 | cs.render = true;
34 | return;
35 | }
36 | switch (t) {
37 | case 'link':
38 | cs.link = true;
39 | cs.link_label = true;
40 | cs.render = true;
41 | break;
42 | case 'string':
43 | cs.link = true;
44 | cs.link_href = true;
45 | cs.render = true;
46 | break;
47 | case 'comment':
48 | cs.code = true;
49 | cs.render = true;
50 | break;
51 | case 'variable-2':
52 | var text = cm.getLine(currentPos.line);
53 | if (/^\s*\d+\.\s/.test(text)) {
54 | cs.oList = true;
55 | cs.render = true;
56 | } else {
57 | cs.uList = true;
58 | cs.render = true;
59 | }
60 | break;
61 | default:
62 | break;
63 | }
64 | });
65 | return cs;
66 | }
67 |
68 | var operations = {
69 | inlineApply(cm, format) {
70 | var startPoint = cm.getCursor('start');
71 | var endPoint = cm.getCursor('end');
72 | var selection = cm.getSelection();
73 | cm.replaceSelection(format.before + selection + format.after);
74 | startPoint.ch += format.before.length;
75 | endPoint.ch += format.after.length;
76 |
77 | if (format.token === 'link') {
78 | if (selection === '') {
79 | // 如果是新建,则聚焦到内容里面
80 | cm.setSelection(startPoint, cm.getCursor('end') - 2);
81 | } else {
82 | // 如果是选中,则聚焦到连接里面
83 | cm.setSelection(endPoint, cm.getCursor('end') - 1);
84 | }
85 | } else if (format.token === 'image') {
86 | if (selection === '') {
87 | // 如果是新建,则聚焦到内容里面
88 | cm.setSelection(startPoint, cm.getCursor('end') - 2);
89 | } else {
90 | // 如果是选中,则聚焦到连接里面
91 | cm.setSelection(endPoint, cm.getCursor('end'));
92 | }
93 | } else {
94 | cm.setSelection(startPoint, endPoint);
95 | }
96 |
97 | cm.focus();
98 | },
99 | inlineRemove(cm, format) {
100 | var startPoint = cm.getCursor('start');
101 | var endPoint = cm.getCursor('end');
102 | var line = cm.getLine(startPoint.line);
103 | var startPos = startPoint.ch;
104 |
105 | while (startPos) {
106 | if (line.substr(startPos, format.before.length) === format.before) {
107 | break;
108 | }
109 | startPos--;
110 | }
111 |
112 | var endPos = endPoint.ch;
113 | while (endPos <= line.length) {
114 | if (line.substr(endPos, format.after.length) === format.after) {
115 | break;
116 | }
117 | endPos++;
118 | }
119 |
120 | var start = line.slice(0, startPos);
121 | var mid = line.slice(startPos + format.before.length, endPos);
122 | var end = line.slice(endPos + format.after.length);
123 | cm.replaceRange(start + mid + end, { line: startPoint.line, ch: 0 }, { line: startPoint.line, ch: line.length + 1 });
124 | cm.setSelection({ line: startPoint.line, ch: start.length }, { line: startPoint.line, ch: (start + mid).length });
125 | cm.focus();
126 | },
127 | blockApply(cm, format) {
128 | var startPoint = cm.getCursor('start');
129 | var line = cm.getLine(startPoint.line);
130 | var text = format.before + ' ' + (line.length ? line : format.placeholder);
131 | cm.replaceRange(text, { line: startPoint.line, ch: 0 }, { line: startPoint.line, ch: line.length + 1 });
132 | cm.setSelection({ line: startPoint.line, ch: format.before.length + 1 }, { line: startPoint.line, ch: text.length });
133 | cm.focus();
134 | },
135 | blockRemove(cm, format) {
136 | var startPoint = cm.getCursor('start');
137 | var line = cm.getLine(startPoint.line);
138 | var text = line.replace(format.re, '');
139 | cm.replaceRange(text, { line: startPoint.line, ch: 0 }, { line: startPoint.line, ch: line.length + 1 });
140 | cm.setSelection({ line: startPoint.line, ch: 0 }, { line: startPoint.line, ch: text.length });
141 | cm.focus();
142 | }
143 | };
144 |
145 | export function applyFormat(cm, key) {
146 | var cs = getCursorState(cm);
147 | var format = FORMATS[key];
148 | operations[format.type + (cs[key] ? 'Remove' : 'Apply')](cm, format);
149 | }
150 |
--------------------------------------------------------------------------------
/src/utils/localStorage.js:
--------------------------------------------------------------------------------
1 | const storageFactory = (storage) => {
2 | return {
3 | get: (value) => {
4 | try {
5 | return JSON.parse((storage.getItem(value) || '{}'));
6 | } catch (e) {
7 | return storage.getItem(value);
8 | }
9 | },
10 | set: (key, value) => {
11 | let currentValue = value;
12 | if (typeof value !== 'string') {
13 | currentValue = JSON.stringify(value);
14 | }
15 | return storage.setItem(key, currentValue);
16 | },
17 | remove: (key) => {
18 | return storage.removeItem(key);
19 | },
20 | };
21 | };
22 |
23 | const localStorageService = storageFactory(window.localStorage);
24 |
25 | module.exports = {
26 | set: localStorageService.set,
27 | get: localStorageService.get,
28 | remove: localStorageService.remove,
29 | session: storageFactory(window.sessionStorage),
30 | };
31 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "assert": true,
7 | },
8 | "rules": {
9 | "no-script-url": 1,
10 | "no-unused-expressions": 0,
11 | "react/no-multi-comp": 0
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/test.storage.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var storage = require('../src/utils/localStorage');
4 |
5 | describe('Test Storage', function() {
6 |
7 | it('should get storage', function(done) {
8 | console.log(done);
9 | done();
10 | });
11 |
12 | });
13 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | require("./register-babel");
2 | var config = require("./webpack/webpack.config");
3 | var result = config();
4 | module.exports = result;
5 |
--------------------------------------------------------------------------------
/webpack/strategies/development.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | export default (config, options) => {
4 | if (options.development) {
5 | config = _.extend({}, config, {
6 | devtool: "cheap-module-eval-source-map"
7 | });
8 | return config;
9 | }
10 |
11 | return config;
12 | };
13 |
--------------------------------------------------------------------------------
/webpack/strategies/index.js:
--------------------------------------------------------------------------------
1 | import development from "./development";
2 | import version from "./version";
3 | import optimize from "./optimize";
4 | import style from "./style";
5 |
6 | export default [
7 | development,
8 | optimize,
9 | version,
10 | style,
11 | ];
12 |
--------------------------------------------------------------------------------
/webpack/strategies/optimize.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 | import { optimize, NoErrorsPlugin, HotModuleReplacementPlugin } from "webpack";
3 |
4 | export default (config, options) => {
5 | if (options.optimize) {
6 | config = _.extend({}, config, {
7 | output: _.extend({}, config.output, {
8 | filename: "[name].min.js",
9 | }),
10 | });
11 | config.plugins = config.plugins.concat([
12 | new HotModuleReplacementPlugin(),
13 | new optimize.UglifyJsPlugin({
14 | compressor: {
15 | warnings: false
16 | }
17 | }),
18 | new optimize.OccurenceOrderPlugin(),
19 | new optimize.DedupePlugin(),
20 | new NoErrorsPlugin(),
21 | ]);
22 | return config;
23 | }
24 |
25 | return config;
26 | };
27 |
--------------------------------------------------------------------------------
/webpack/strategies/style.js:
--------------------------------------------------------------------------------
1 | import ExtractTextPlugin, { extract } from "extract-text-webpack-plugin";
2 | const path = require('path');
3 | const pkg = require(path.join(process.cwd(), 'package.json'));
4 |
5 | export default (config, options) => {
6 | const stylesheetLoaders = [
7 | { test: /\.css/, loader: "css" },
8 | { test: /\.less/, loader: 'css!less?{"sourceMap":true,"modifyVars":' + JSON.stringify(pkg.theme || {})+'}' },
9 | ];
10 |
11 | let loaders = [];
12 | for (let loader of stylesheetLoaders) {
13 | if (options.prerender) {
14 | loader.loader = "null";
15 | } else if (options.separateStylesheet) {
16 | loader.loader = extract("style", loader.loader);
17 | } else {
18 | loader.loader = `style!${loader.loader}`;
19 | }
20 | loaders.push(loader);
21 | }
22 |
23 | config.module.loaders = config.module.loaders.concat(loaders);
24 |
25 | if (options.separateStylesheet) {
26 | config.plugins.push(new ExtractTextPlugin("app.css"));
27 | }
28 | return config;
29 | };
30 |
--------------------------------------------------------------------------------
/webpack/strategies/version.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { execSync } from "child_process";
3 | import { join } from "path";
4 |
5 | import { version } from "../../package.json";
6 |
7 | export default (config, options) => {
8 | if (!options.prerender) {
9 | let plugin = function() {
10 | this.plugin("done", function(stats) {
11 | let jsonStats = stats.toJson({
12 | chunkModules: true,
13 | exclude: options.excludeFromStats,
14 | });
15 | jsonStats.publicPath = options.publicPath;
16 | jsonStats.appVersion = version;
17 | jsonStats.appCommit = execSync("git rev-parse --short HEAD").toString();
18 |
19 | const folderPath = join(__dirname, "../../", "build");
20 | if (!fs.existsSync(folderPath)) {
21 | fs.mkdirSync(folderPath);
22 | }
23 | fs.writeFileSync(join(folderPath, "stats.json"), JSON.stringify(jsonStats));
24 | });
25 | };
26 | config.plugins.push(plugin);
27 | }
28 | return config;
29 | };
30 |
--------------------------------------------------------------------------------
/webpack/webpack.config.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 | import webpack from "webpack";
3 | import strategies from "./strategies";
4 | import yargs from "yargs";
5 | import pkg from '../package.json';
6 |
7 | const argv = yargs
8 | .alias("p", "optimize-minimize")
9 | .alias("d", "debug")
10 | .alias("s", "dev-server")
11 | .argv;
12 |
13 | const defaultOptions = {
14 | development: argv.debug,
15 | docs: false,
16 | test: false,
17 | optimize: argv.optimizeMinimize,
18 | devServer: argv.devServer,
19 | separateStylesheet: argv.separateStylesheet,
20 | prerender: argv.prerender,
21 | };
22 |
23 | export default (options) => {
24 | options = _.merge({}, defaultOptions, options);
25 |
26 | options.hotPort = 2992;
27 | options.publicPath = options.devServer ? "/_assets/" : "";
28 | const environment = options.test || options.development ? "development" : "production";
29 | const babelLoader = "babel";
30 | const reactLoader = options.development ? `react-hot!${babelLoader}` : babelLoader;
31 | const chunkFilename = (options.devServer ? "[id].js" : "[name].js") +
32 | (options.longTermCaching && !options.prerender ? "?[chunkhash]" : "");
33 |
34 | options.excludeFromStats = [
35 | /node_modules[\\\/]react(-router)?[\\\/]/,
36 | ];
37 |
38 | const config = {
39 | entry: {
40 | app: './src/entry/App.jsx'
41 | },
42 | output: {
43 | path: "./public/" + pkg.version,
44 | filename: "[name].js",
45 | chunkFilename: chunkFilename,
46 | publicPath: options.publicPath,
47 | sourceMapFilename: "debugging/[file].map",
48 | },
49 |
50 | externals: [
51 | ],
52 |
53 | resolve: {
54 | extensions: ["", ".js", ".jsx"],
55 | },
56 |
57 | module: {
58 | noParse: [/autoit.js/],
59 | loaders: [
60 | { test: /\.(js|jsx)/, loader: reactLoader, exclude: /node_modules/},
61 | { test: /\.json/, loader: "json" },
62 | { test: /\.(woff|woff2)/, loader: "url?limit=100000" },
63 | { test: /\.(png|jpg|jpeg|gif|svg)/, loader: "url?limit=100000" },
64 | { test: /\.(ttf|eot)/, loader: "file" },
65 | ],
66 | },
67 |
68 | plugins: [
69 | new webpack.PrefetchPlugin("react"),
70 | new webpack.PrefetchPlugin("react-router"),
71 | new webpack.PrefetchPlugin("react/lib/ReactComponentBrowserEnvironment"),
72 | new webpack.DefinePlugin({
73 | "process.env": {
74 | NODE_ENV: JSON.stringify(environment),
75 | },
76 | }),
77 | ],
78 |
79 | devServer: {
80 | host: "localhost",
81 | port: options.hotPort,
82 | stats: {
83 | exclude: options.excludeFromStats,
84 | },
85 | },
86 | };
87 |
88 | return strategies.reduce((conf, strategy) => {
89 | return strategy(conf, options);
90 | }, config);
91 | };
92 |
--------------------------------------------------------------------------------