add changes from v0.3
This commit is contained in:
@@ -1,2 +1 @@
|
|||||||
node_modules
|
node_modules
|
||||||
db
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
.next
|
.next
|
||||||
styles
|
|
||||||
config
|
|
||||||
32
.eslintrc.js
Normal file
32
.eslintrc.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
module.exports = {
|
||||||
|
globals: {
|
||||||
|
app: false,
|
||||||
|
},
|
||||||
|
parser: 'babel-eslint',
|
||||||
|
plugins: ['react'],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 6,
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: '16.7',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
es6: true,
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
mocha: true,
|
||||||
|
},
|
||||||
|
extends: ['eslint:recommended', 'plugin:react/recommended'],
|
||||||
|
rules: {
|
||||||
|
'no-console': 0,
|
||||||
|
'react/prop-types': 0,
|
||||||
|
'react/react-in-jsx-scope': 0,
|
||||||
|
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"globals": {
|
|
||||||
"app": false
|
|
||||||
},
|
|
||||||
"parser": "babel-eslint",
|
|
||||||
"plugins": [
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 6,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"es6": true,
|
|
||||||
"browser": true,
|
|
||||||
"node": true,
|
|
||||||
"mocha": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"react/prop-types": 0,
|
|
||||||
"react/react-in-jsx-scope": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,6 +4,7 @@ logs
|
|||||||
|
|
||||||
# Next build data (only add manually)
|
# Next build data (only add manually)
|
||||||
.next
|
.next
|
||||||
|
public/sw.js
|
||||||
|
|
||||||
# cloudflare ips cache file
|
# cloudflare ips cache file
|
||||||
config/cfIPs.json
|
config/cfIPs.json
|
||||||
@@ -12,6 +13,8 @@ config/cfIPs.json
|
|||||||
pids
|
pids
|
||||||
*.pid
|
*.pid
|
||||||
*.seed
|
*.seed
|
||||||
|
.sessions
|
||||||
|
.cache
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
lib-cov
|
lib-cov
|
||||||
@@ -114,6 +117,4 @@ $RECYCLE.BIN/
|
|||||||
*.lnk
|
*.lnk
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
lib/
|
|
||||||
db
|
|
||||||
kb
|
kb
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
MdYEkay1bViLxxtsssdFt
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
{
|
|
||||||
"devFiles": [],
|
|
||||||
"pages": {
|
|
||||||
"/_app": [
|
|
||||||
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
|
|
||||||
"static/chunks/commons.633cb95994571bd38b02.js",
|
|
||||||
"static/runtime/main-a0940d6708920bf8234a.js"
|
|
||||||
],
|
|
||||||
"/edit": [
|
|
||||||
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
|
|
||||||
"static/chunks/commons.633cb95994571bd38b02.js",
|
|
||||||
"static/runtime/main-a0940d6708920bf8234a.js"
|
|
||||||
],
|
|
||||||
"/index": [
|
|
||||||
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
|
|
||||||
"static/chunks/commons.633cb95994571bd38b02.js",
|
|
||||||
"static/runtime/main-a0940d6708920bf8234a.js"
|
|
||||||
],
|
|
||||||
"/k": [
|
|
||||||
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
|
|
||||||
"static/chunks/commons.633cb95994571bd38b02.js",
|
|
||||||
"static/runtime/main-a0940d6708920bf8234a.js"
|
|
||||||
],
|
|
||||||
"/new": [
|
|
||||||
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
|
|
||||||
"static/chunks/commons.633cb95994571bd38b02.js",
|
|
||||||
"static/runtime/main-a0940d6708920bf8234a.js"
|
|
||||||
],
|
|
||||||
"/settings": [
|
|
||||||
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
|
|
||||||
"static/chunks/commons.633cb95994571bd38b02.js",
|
|
||||||
"static/runtime/main-a0940d6708920bf8234a.js"
|
|
||||||
],
|
|
||||||
"/_error": [
|
|
||||||
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
|
|
||||||
"static/chunks/commons.633cb95994571bd38b02.js",
|
|
||||||
"static/runtime/main-a0940d6708920bf8234a.js"
|
|
||||||
],
|
|
||||||
"/": [
|
|
||||||
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
|
|
||||||
"static/chunks/commons.633cb95994571bd38b02.js",
|
|
||||||
"static/runtime/main-a0940d6708920bf8234a.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,848 +0,0 @@
|
|||||||
{
|
|
||||||
"modules": {
|
|
||||||
"byIdentifier": {
|
|
||||||
"node_modules/react/index.js": 0,
|
|
||||||
"node_modules/glamor/lib/index.js": 1,
|
|
||||||
"node_modules/@babel/runtime/regenerator/index.js": 2,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/getUrl.js": 3,
|
|
||||||
"node_modules/core-js/library/modules/_core.js": 4,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/interopRequireDefault.js": 5,
|
|
||||||
"node_modules/prop-types/index.js": 6,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/theme.js": 7,
|
|
||||||
"node_modules/core-js/library/modules/_export.js": 8,
|
|
||||||
"node_modules/core-js/library/modules/_global.js": 9,
|
|
||||||
"node_modules/core-js/library/modules/_wks.js": 10,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/getJwt.js": 11,
|
|
||||||
"node_modules/react-redux/es/index.js 67b03e0c128d2b1ee84267ae4ef3254d": 12,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/redux/actions/userAct.js": 13,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/classCallCheck.js": 14,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/createClass.js": 15,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/mapUser.js": 16,
|
|
||||||
"node_modules/core-js/library/modules/_is-object.js": 17,
|
|
||||||
"node_modules/isomorphic-unfetch/browser.js": 18,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Page.js 7cd3bec4a0252faf8f91892a2f798527": 19,
|
|
||||||
"node_modules/core-js/library/modules/_object-dp.js": 20,
|
|
||||||
"node_modules/next/dynamic.js": 21,
|
|
||||||
"node_modules/next/router.js": 22,
|
|
||||||
"node_modules/core-js/library/modules/_an-object.js": 23,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/defineProperty.js": 24,
|
|
||||||
"node_modules/core-js/library/modules/_descriptors.js": 25,
|
|
||||||
"node_modules/core-js/library/modules/_ctx.js": 26,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/possibleConstructorReturn.js": 27,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/getPrototypeOf.js": 28,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/inherits.js": 29,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/PaddedRow.js": 30,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/redux/store.js 4e1280ed09dd33013b4dfa4dd70e85b4": 31,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/interopRequireWildcard.js": 32,
|
|
||||||
"node_modules/core-js/library/modules/_fails.js": 33,
|
|
||||||
"node_modules/core-js/library/modules/_hide.js": 34,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/objectSpread.js": 35,
|
|
||||||
"node_modules/core-js/library/modules/es6.string.iterator.js": 36,
|
|
||||||
"node_modules/next/dist/lib/utils.js": 37,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/updStateFromId.js": 38,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Markdown.js d5b5492a768bba52c33c5b22a90aa8a1": 39,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Spinner.js": 40,
|
|
||||||
"node_modules/core-js/library/modules/_to-iobject.js": 41,
|
|
||||||
"node_modules/core-js/library/modules/_has.js": 42,
|
|
||||||
"node_modules/core-js/library/modules/_a-function.js": 43,
|
|
||||||
"node_modules/core-js/library/modules/_to-object.js": 44,
|
|
||||||
"node_modules/core-js/library/modules/_iterators.js": 45,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/typeof.js": 46,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/checkDirParts.js": 47,
|
|
||||||
"node_modules/core-js/library/modules/_property-desc.js": 48,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/promise.js": 49,
|
|
||||||
"node_modules/core-js/library/modules/web.dom.iterable.js": 50,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/set.js": 51,
|
|
||||||
"node_modules/next/link.js": 52,
|
|
||||||
"node_modules/redux/es/redux.js": 53,
|
|
||||||
"node_modules/core-js/library/modules/_cof.js": 54,
|
|
||||||
"node_modules/core-js/library/modules/_library.js": 55,
|
|
||||||
"node_modules/core-js/library/modules/_set-to-string-tag.js": 56,
|
|
||||||
"node_modules/core-js/library/modules/_object-keys.js": 57,
|
|
||||||
"node_modules/core-js/library/modules/_to-length.js": 58,
|
|
||||||
"node_modules/core-js/library/modules/_object-create.js": 59,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/object/keys.js": 60,
|
|
||||||
"node_modules/core-js/library/modules/_classof.js": 61,
|
|
||||||
"node_modules/core-js/library/modules/_for-of.js": 62,
|
|
||||||
"node_modules/object-assign/index.js": 63,
|
|
||||||
"node_modules/next/dist/lib/router/index.js": 64,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/assertThisInitialized.js": 65,
|
|
||||||
"node_modules/css-in-js-utils/lib/isPrefixedValue.js": 66,
|
|
||||||
"node_modules/next/dist/lib/dynamic.js": 67,
|
|
||||||
"node_modules/url/url.js": 68,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/AddDoc.js": 69,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/MngDoc.js cfa406d5564443825298c8c4c761c7c2": 70,
|
|
||||||
"node_modules/core-js/library/modules/_object-pie.js": 71,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/object/define-property.js": 72,
|
|
||||||
"node_modules/core-js/library/modules/_uid.js": 73,
|
|
||||||
"node_modules/@babel/runtime-corejs2/regenerator/index.js": 74,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/asyncToGenerator.js": 75,
|
|
||||||
"node_modules/core-js/library/modules/core.get-iterator-method.js": 76,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/array/is-array.js": 77,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/object/assign.js": 78,
|
|
||||||
"node_modules/next/dist/lib/loadable.js": 79,
|
|
||||||
"node_modules/invariant/browser.js": 80,
|
|
||||||
"node_modules/core-js/library/modules/_iobject.js": 81,
|
|
||||||
"node_modules/core-js/library/modules/_defined.js": 82,
|
|
||||||
"node_modules/core-js/library/modules/_object-gopd.js": 83,
|
|
||||||
"node_modules/core-js/library/modules/_to-primitive.js": 84,
|
|
||||||
"node_modules/core-js/library/modules/_dom-create.js": 85,
|
|
||||||
"node_modules/core-js/library/modules/_object-sap.js": 86,
|
|
||||||
"node_modules/core-js/library/modules/_meta.js": 87,
|
|
||||||
"node_modules/core-js/library/modules/_shared.js": 88,
|
|
||||||
"node_modules/core-js/library/modules/_wks-ext.js": 89,
|
|
||||||
"node_modules/core-js/library/modules/_wks-define.js": 90,
|
|
||||||
"node_modules/core-js/library/modules/_to-integer.js": 91,
|
|
||||||
"node_modules/core-js/library/modules/_shared-key.js": 92,
|
|
||||||
"node_modules/core-js/library/modules/_enum-bug-keys.js": 93,
|
|
||||||
"node_modules/core-js/library/modules/_object-gops.js": 94,
|
|
||||||
"node_modules/core-js/library/modules/_is-array.js": 95,
|
|
||||||
"node_modules/core-js/library/modules/es6.object.to-string.js": 96,
|
|
||||||
"node_modules/core-js/library/modules/_iter-define.js": 97,
|
|
||||||
"node_modules/core-js/library/modules/_an-instance.js": 98,
|
|
||||||
"node_modules/core-js/library/modules/_iter-call.js": 99,
|
|
||||||
"node_modules/core-js/library/modules/_is-array-iter.js": 100,
|
|
||||||
"node_modules/core-js/library/modules/_new-promise-capability.js": 101,
|
|
||||||
"node_modules/core-js/library/modules/_redefine-all.js": 102,
|
|
||||||
"node_modules/core-js/library/modules/_iter-detect.js": 103,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/slicedToArray.js": 104,
|
|
||||||
"node_modules/webpack/buildin/global.js": 105,
|
|
||||||
"node_modules/next/dist/lib/EventEmitter.js": 106,
|
|
||||||
"node_modules/next/dist/lib/head.js": 107,
|
|
||||||
"node_modules/next/dist/lib/side-effect.js": 108,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/toConsumableArray.js": 109,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/arrayWithoutHoles.js": 110,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/iterableToArray.js": 111,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/array/from.js": 112,
|
|
||||||
"node_modules/core-js/library/fn/array/from.js": 113,
|
|
||||||
"node_modules/core-js/library/modules/es6.array.from.js": 114,
|
|
||||||
"node_modules/core-js/library/modules/_create-property.js": 115,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/is-iterable.js": 116,
|
|
||||||
"node_modules/core-js/library/fn/is-iterable.js": 117,
|
|
||||||
"node_modules/core-js/library/modules/core.is-iterable.js": 118,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/nonIterableSpread.js": 119,
|
|
||||||
"node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js": 120,
|
|
||||||
"node_modules/symbol-observable/es/index.js": 121,
|
|
||||||
"node_modules/next/app.js": 122,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/object/get-own-property-descriptor.js": 123,
|
|
||||||
"node_modules/core-js/library/modules/_ie8-dom-define.js": 124,
|
|
||||||
"node_modules/core-js/library/modules/es6.symbol.js": 125,
|
|
||||||
"node_modules/core-js/library/modules/_redefine.js": 126,
|
|
||||||
"node_modules/core-js/library/modules/_object-keys-internal.js": 127,
|
|
||||||
"node_modules/core-js/library/modules/_html.js": 128,
|
|
||||||
"node_modules/core-js/library/modules/_object-gopn.js": 129,
|
|
||||||
"node_modules/regenerator-runtime/runtime-module.js": 130,
|
|
||||||
"node_modules/core-js/library/modules/_object-gpo.js": 131,
|
|
||||||
"node_modules/core-js/library/modules/_iter-step.js": 132,
|
|
||||||
"node_modules/core-js/library/modules/_species-constructor.js": 133,
|
|
||||||
"node_modules/core-js/library/modules/_task.js": 134,
|
|
||||||
"node_modules/core-js/library/modules/_invoke.js": 135,
|
|
||||||
"node_modules/core-js/library/modules/_perform.js": 136,
|
|
||||||
"node_modules/core-js/library/modules/_promise-resolve.js": 137,
|
|
||||||
"node_modules/core-js/library/modules/_set-species.js": 138,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/setPrototypeOf.js": 139,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/object/set-prototype-of.js": 140,
|
|
||||||
"node_modules/core-js/library/modules/_validate-collection.js": 141,
|
|
||||||
"node_modules/webpack/buildin/module.js": 142,
|
|
||||||
"node_modules/next/dist/lib/shallow-equals.js": 143,
|
|
||||||
"node_modules/glamor/lib/CSSPropertyOperations/index.js": 144,
|
|
||||||
"node_modules/fbjs/lib/warning.js": 145,
|
|
||||||
"node_modules/inline-style-prefixer/utils/capitalizeString.js": 146,
|
|
||||||
"node_modules/isomorphic-unfetch/node_modules/unfetch/dist/unfetch.es.js": 147,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/keys.js": 148,
|
|
||||||
"node_modules/next/head.js": 149,
|
|
||||||
"node_modules/symbol-observable/es/ponyfill.js": 150,
|
|
||||||
"node_modules/lodash-es/_freeGlobal.js": 151,
|
|
||||||
"node_modules/react-paginate/dist/index.js": 152,
|
|
||||||
"node_modules/process/browser.js": 153,
|
|
||||||
"multi node_modules/next/dist/client/next": 154,
|
|
||||||
"node_modules/next/dist/client/next.js": 155,
|
|
||||||
"node_modules/core-js/library/fn/object/get-own-property-descriptor.js": 156,
|
|
||||||
"node_modules/core-js/library/modules/es6.object.get-own-property-descriptor.js": 157,
|
|
||||||
"node_modules/core-js/library/fn/object/define-property.js": 158,
|
|
||||||
"node_modules/core-js/library/modules/es6.object.define-property.js": 159,
|
|
||||||
"node_modules/next/dist/client/index.js": 160,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/object/get-own-property-symbols.js": 161,
|
|
||||||
"node_modules/core-js/library/fn/object/get-own-property-symbols.js": 162,
|
|
||||||
"node_modules/core-js/library/modules/_enum-keys.js": 163,
|
|
||||||
"node_modules/core-js/library/modules/_array-includes.js": 164,
|
|
||||||
"node_modules/core-js/library/modules/_to-absolute-index.js": 165,
|
|
||||||
"node_modules/core-js/library/modules/_object-dps.js": 166,
|
|
||||||
"node_modules/core-js/library/modules/_object-gopn-ext.js": 167,
|
|
||||||
"node_modules/core-js/library/fn/object/keys.js": 168,
|
|
||||||
"node_modules/core-js/library/modules/es6.object.keys.js": 169,
|
|
||||||
"node_modules/regenerator-runtime/runtime.js": 170,
|
|
||||||
"node_modules/core-js/library/fn/promise.js": 171,
|
|
||||||
"node_modules/core-js/library/modules/_string-at.js": 172,
|
|
||||||
"node_modules/core-js/library/modules/_iter-create.js": 173,
|
|
||||||
"node_modules/core-js/library/modules/es6.array.iterator.js": 174,
|
|
||||||
"node_modules/core-js/library/modules/_add-to-unscopables.js": 175,
|
|
||||||
"node_modules/core-js/library/modules/es6.promise.js": 176,
|
|
||||||
"node_modules/core-js/library/modules/_microtask.js": 177,
|
|
||||||
"node_modules/core-js/library/modules/_user-agent.js": 178,
|
|
||||||
"node_modules/core-js/library/modules/es7.promise.finally.js": 179,
|
|
||||||
"node_modules/core-js/library/modules/es7.promise.try.js": 180,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/arrayWithHoles.js": 181,
|
|
||||||
"node_modules/core-js/library/fn/array/is-array.js": 182,
|
|
||||||
"node_modules/core-js/library/modules/es6.array.is-array.js": 183,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/iterableToArrayLimit.js": 184,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/get-iterator.js": 185,
|
|
||||||
"node_modules/core-js/library/fn/get-iterator.js": 186,
|
|
||||||
"node_modules/core-js/library/modules/core.get-iterator.js": 187,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/nonIterableRest.js": 188,
|
|
||||||
"node_modules/react/cjs/react.production.min.js": 189,
|
|
||||||
"node_modules/react-dom/index.js": 190,
|
|
||||||
"node_modules/react-dom/cjs/react-dom.production.min.js": 191,
|
|
||||||
"node_modules/schedule/index.js": 192,
|
|
||||||
"node_modules/schedule/cjs/schedule.production.min.js": 193,
|
|
||||||
"node_modules/next/dist/client/head-manager.js": 194,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/symbol/iterator.js": 195,
|
|
||||||
"node_modules/core-js/library/fn/symbol/iterator.js": 196,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/symbol.js": 197,
|
|
||||||
"node_modules/core-js/library/fn/symbol/index.js": 198,
|
|
||||||
"node_modules/core-js/library/modules/es7.symbol.async-iterator.js": 199,
|
|
||||||
"node_modules/core-js/library/modules/es7.symbol.observable.js": 200,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/construct.js": 201,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/reflect/construct.js": 202,
|
|
||||||
"node_modules/core-js/library/fn/reflect/construct.js": 203,
|
|
||||||
"node_modules/core-js/library/modules/es6.reflect.construct.js": 204,
|
|
||||||
"node_modules/core-js/library/modules/_bind.js": 205,
|
|
||||||
"node_modules/core-js/library/fn/object/set-prototype-of.js": 206,
|
|
||||||
"node_modules/core-js/library/modules/es6.object.set-prototype-of.js": 207,
|
|
||||||
"node_modules/core-js/library/modules/_set-proto.js": 208,
|
|
||||||
"node_modules/next/dist/lib/router/router.js": 209,
|
|
||||||
"node_modules/core-js/library/fn/set.js": 210,
|
|
||||||
"node_modules/core-js/library/modules/es6.set.js": 211,
|
|
||||||
"node_modules/core-js/library/modules/_collection-strong.js": 212,
|
|
||||||
"node_modules/core-js/library/modules/_collection.js": 213,
|
|
||||||
"node_modules/core-js/library/modules/_array-methods.js": 214,
|
|
||||||
"node_modules/core-js/library/modules/_array-species-create.js": 215,
|
|
||||||
"node_modules/core-js/library/modules/_array-species-constructor.js": 216,
|
|
||||||
"node_modules/core-js/library/modules/es7.set.to-json.js": 217,
|
|
||||||
"node_modules/core-js/library/modules/_collection-to-json.js": 218,
|
|
||||||
"node_modules/core-js/library/modules/_array-from-iterable.js": 219,
|
|
||||||
"node_modules/core-js/library/modules/es7.set.of.js": 220,
|
|
||||||
"node_modules/core-js/library/modules/_set-collection-of.js": 221,
|
|
||||||
"node_modules/core-js/library/modules/es7.set.from.js": 222,
|
|
||||||
"node_modules/core-js/library/modules/_set-collection-from.js": 223,
|
|
||||||
"node_modules/punycode/punycode.js": 224,
|
|
||||||
"node_modules/url/util.js": 225,
|
|
||||||
"node_modules/querystring-es3/index.js": 226,
|
|
||||||
"node_modules/querystring-es3/decode.js": 227,
|
|
||||||
"node_modules/querystring-es3/encode.js": 228,
|
|
||||||
"node_modules/next/dist/lib/p-queue.js": 229,
|
|
||||||
"node_modules/core-js/library/fn/object/assign.js": 230,
|
|
||||||
"node_modules/core-js/library/modules/es6.object.assign.js": 231,
|
|
||||||
"node_modules/core-js/library/modules/_object-assign.js": 232,
|
|
||||||
"node_modules/next/dist/lib/router/with-router.js": 233,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/object/get-prototype-of.js": 234,
|
|
||||||
"node_modules/core-js/library/fn/object/get-prototype-of.js": 235,
|
|
||||||
"node_modules/core-js/library/modules/es6.object.get-prototype-of.js": 236,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/object/create.js": 237,
|
|
||||||
"node_modules/core-js/library/fn/object/create.js": 238,
|
|
||||||
"node_modules/core-js/library/modules/es6.object.create.js": 239,
|
|
||||||
"node_modules/prop-types/factoryWithThrowingShims.js": 240,
|
|
||||||
"node_modules/prop-types/lib/ReactPropTypesSecret.js": 241,
|
|
||||||
"node_modules/next/dist/lib/page-loader.js": 242,
|
|
||||||
"node_modules/next/dist/lib/asset.js": 243,
|
|
||||||
"node_modules/next/dist/lib/runtime-config.js": 244,
|
|
||||||
"node_modules/next/dist/client/error-boundary.js": 245,
|
|
||||||
"multi ./pages/_app.js": 246,
|
|
||||||
"node_modules/glamor/lib/sheet.js": 247,
|
|
||||||
"node_modules/fbjs/lib/camelizeStyleName.js": 248,
|
|
||||||
"node_modules/fbjs/lib/camelize.js": 249,
|
|
||||||
"node_modules/glamor/lib/CSSPropertyOperations/dangerousStyleValue.js": 250,
|
|
||||||
"node_modules/glamor/lib/CSSPropertyOperations/CSSProperty.js": 251,
|
|
||||||
"node_modules/fbjs/lib/emptyFunction.js": 252,
|
|
||||||
"node_modules/fbjs/lib/hyphenateStyleName.js": 253,
|
|
||||||
"node_modules/fbjs/lib/hyphenate.js": 254,
|
|
||||||
"node_modules/fbjs/lib/memoizeStringOnly.js": 255,
|
|
||||||
"node_modules/glamor/lib/clean.js": 256,
|
|
||||||
"node_modules/glamor/lib/plugins.js": 257,
|
|
||||||
"node_modules/glamor/lib/prefixer.js": 258,
|
|
||||||
"node_modules/inline-style-prefixer/static/staticData.js": 259,
|
|
||||||
"node_modules/inline-style-prefixer/utils/prefixProperty.js": 260,
|
|
||||||
"node_modules/inline-style-prefixer/utils/prefixValue.js": 261,
|
|
||||||
"node_modules/inline-style-prefixer/static/plugins/cursor.js": 262,
|
|
||||||
"node_modules/inline-style-prefixer/static/plugins/crossFade.js": 263,
|
|
||||||
"node_modules/inline-style-prefixer/static/plugins/filter.js": 264,
|
|
||||||
"node_modules/inline-style-prefixer/static/plugins/flex.js": 265,
|
|
||||||
"node_modules/inline-style-prefixer/static/plugins/flexboxOld.js": 266,
|
|
||||||
"node_modules/inline-style-prefixer/static/plugins/gradient.js": 267,
|
|
||||||
"node_modules/inline-style-prefixer/static/plugins/imageSet.js": 268,
|
|
||||||
"node_modules/inline-style-prefixer/static/plugins/position.js": 269,
|
|
||||||
"node_modules/inline-style-prefixer/static/plugins/sizing.js": 270,
|
|
||||||
"node_modules/inline-style-prefixer/static/plugins/transition.js": 271,
|
|
||||||
"node_modules/css-in-js-utils/lib/hyphenateProperty.js": 272,
|
|
||||||
"node_modules/hyphenate-style-name/index.js": 273,
|
|
||||||
"node_modules/glamor/lib/hash.js": 274,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/milligram.js": 275,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/Roboto.js": 276,
|
|
||||||
"node_modules/url-join/lib/url-join.js": 277,
|
|
||||||
"node_modules/webpack/buildin/harmony-module.js": 278,
|
|
||||||
"node_modules/next/dist/lib/app.js": 279,
|
|
||||||
"node_modules/@babel/runtime-corejs2/helpers/extends.js": 280,
|
|
||||||
"multi ./pages/edit.js": 281,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/edit.js": 282,
|
|
||||||
"node_modules/next/dist/lib/link.js": 283,
|
|
||||||
"node_modules/@babel/runtime-corejs2/core-js/json/stringify.js": 284,
|
|
||||||
"node_modules/core-js/library/fn/json/stringify.js": 285,
|
|
||||||
"multi ./pages/index.js": 286,
|
|
||||||
"node_modules/react-paginate/dist/PaginationBoxView.js": 287,
|
|
||||||
"node_modules/react-paginate/dist/PageView.js": 288,
|
|
||||||
"node_modules/react-paginate/dist/BreakView.js": 289,
|
|
||||||
"multi ./pages/k.js": 290,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/k.js": 291,
|
|
||||||
"multi ./pages/new.js": 292,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/new.js": 293,
|
|
||||||
"multi ./pages/settings.js": 294,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/settings.js": 295,
|
|
||||||
"multi node_modules/next/dist/pages/_error.js": 296,
|
|
||||||
"node_modules/next/dist/pages/_error.js": 297,
|
|
||||||
"node_modules/next/error.js": 298,
|
|
||||||
"node_modules/next/dist/lib/error.js": 299,
|
|
||||||
"node_modules/http-status/lib/index.js": 300,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/index.js 63f90c0b7addefbc51a5877f453fdcbd": 301,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/_app.js 1362119e6d3d895366421ac2b14cb639": 302,
|
|
||||||
"node_modules/is-whitespace-character/index.js": 303,
|
|
||||||
"node_modules/xtend/immutable.js": 304,
|
|
||||||
"node_modules/trim/index.js": 305,
|
|
||||||
"node_modules/codemirror/lib/codemirror.js": 306,
|
|
||||||
"node_modules/is-decimal/index.js": 307,
|
|
||||||
"node_modules/unist-util-visit/index.js": 308,
|
|
||||||
"node_modules/parse-entities/index.js": 309,
|
|
||||||
"node_modules/repeat-string/index.js": 310,
|
|
||||||
"node_modules/trim-trailing-lines/index.js": 311,
|
|
||||||
"node_modules/remark-parse/lib/util/interrupt.js": 312,
|
|
||||||
"node_modules/remark-parse/lib/util/normalize.js": 313,
|
|
||||||
"node_modules/path-browserify/index.js": 314,
|
|
||||||
"node_modules/is-alphabetical/index.js": 315,
|
|
||||||
"node_modules/remark-parse/lib/defaults.js": 316,
|
|
||||||
"node_modules/remark-parse/lib/util/get-indentation.js": 317,
|
|
||||||
"node_modules/remark-parse/lib/util/html.js": 318,
|
|
||||||
"node_modules/remark-parse/lib/locate/tag.js": 319,
|
|
||||||
"node_modules/remark-parse/lib/locate/link.js": 320,
|
|
||||||
"node_modules/codemirror/mode/markdown/markdown.js": 321,
|
|
||||||
"node_modules/codemirror/mode/xml/xml.js": 322,
|
|
||||||
"node_modules/codemirror/mode/meta.js": 323,
|
|
||||||
"node_modules/unified/index.js": 324,
|
|
||||||
"node_modules/extend/index.js": 325,
|
|
||||||
"node_modules/bail/index.js": 326,
|
|
||||||
"node_modules/vfile/index.js": 327,
|
|
||||||
"node_modules/vfile-message/index.js": 328,
|
|
||||||
"node_modules/unist-util-stringify-position/index.js": 329,
|
|
||||||
"node_modules/vfile/core.js": 330,
|
|
||||||
"node_modules/replace-ext/index.js": 331,
|
|
||||||
"node_modules/is-buffer/index.js": 332,
|
|
||||||
"node_modules/trough/index.js": 333,
|
|
||||||
"node_modules/trough/wrap.js": 334,
|
|
||||||
"node_modules/x-is-string/index.js": 335,
|
|
||||||
"node_modules/is-plain-obj/index.js": 336,
|
|
||||||
"node_modules/remark-parse/index.js": 337,
|
|
||||||
"node_modules/unherit/index.js": 338,
|
|
||||||
"node_modules/inherits/inherits_browser.js": 339,
|
|
||||||
"node_modules/remark-parse/lib/parser.js": 340,
|
|
||||||
"node_modules/state-toggle/index.js": 341,
|
|
||||||
"node_modules/vfile-location/index.js": 342,
|
|
||||||
"node_modules/remark-parse/lib/unescape.js": 343,
|
|
||||||
"node_modules/remark-parse/lib/decode.js": 344,
|
|
||||||
"node_modules/character-entities/index.json": 345,
|
|
||||||
"node_modules/character-entities-legacy/index.json": 346,
|
|
||||||
"node_modules/character-reference-invalid/index.json": 347,
|
|
||||||
"node_modules/is-hexadecimal/index.js": 348,
|
|
||||||
"node_modules/is-alphanumerical/index.js": 349,
|
|
||||||
"node_modules/remark-parse/lib/tokenizer.js": 350,
|
|
||||||
"node_modules/remark-parse/lib/set-options.js": 351,
|
|
||||||
"node_modules/markdown-escapes/index.js": 352,
|
|
||||||
"node_modules/remark-parse/lib/block-elements.json": 353,
|
|
||||||
"node_modules/remark-parse/lib/parse.js": 354,
|
|
||||||
"node_modules/unist-util-remove-position/index.js": 355,
|
|
||||||
"node_modules/unist-util-visit/node_modules/unist-util-visit-parents/index.js": 356,
|
|
||||||
"node_modules/unist-util-is/index.js": 357,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/newline.js": 358,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/code-indented.js": 359,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/code-fenced.js": 360,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/blockquote.js": 361,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/heading-atx.js": 362,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/thematic-break.js": 363,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/list.js": 364,
|
|
||||||
"node_modules/remark-parse/lib/util/remove-indentation.js": 365,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/heading-setext.js": 366,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/html-block.js": 367,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/footnote-definition.js": 368,
|
|
||||||
"node_modules/collapse-white-space/index.js": 369,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/definition.js": 370,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/table.js": 371,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/paragraph.js": 372,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/escape.js": 373,
|
|
||||||
"node_modules/remark-parse/lib/locate/escape.js": 374,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/auto-link.js": 375,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/url.js": 376,
|
|
||||||
"node_modules/remark-parse/lib/locate/url.js": 377,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/html-inline.js": 378,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/link.js": 379,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/reference.js": 380,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/strong.js": 381,
|
|
||||||
"node_modules/remark-parse/lib/locate/strong.js": 382,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/emphasis.js": 383,
|
|
||||||
"node_modules/is-word-character/index.js": 384,
|
|
||||||
"node_modules/remark-parse/lib/locate/emphasis.js": 385,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/delete.js": 386,
|
|
||||||
"node_modules/remark-parse/lib/locate/delete.js": 387,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/code-inline.js": 388,
|
|
||||||
"node_modules/remark-parse/lib/locate/code-inline.js": 389,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/break.js": 390,
|
|
||||||
"node_modules/remark-parse/lib/locate/break.js": 391,
|
|
||||||
"node_modules/remark-parse/lib/tokenize/text.js": 392,
|
|
||||||
"node_modules/mdast-add-list-metadata/index.js": 393,
|
|
||||||
"node_modules/unist-util-visit-parents/index.js": 394,
|
|
||||||
"node_modules/react-markdown/lib/plugins/naive-html.js": 395,
|
|
||||||
"node_modules/react-markdown/lib/plugins/disallow-node.js": 396,
|
|
||||||
"node_modules/react-markdown/lib/ast-to-react.js": 397,
|
|
||||||
"node_modules/react-markdown/lib/wrap-table-rows.js": 398,
|
|
||||||
"node_modules/react-markdown/lib/get-definitions.js": 399,
|
|
||||||
"node_modules/react-markdown/lib/uriTransformer.js": 400,
|
|
||||||
"node_modules/react-markdown/lib/renderers.js": 401,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/CodeMirror.js": 402,
|
|
||||||
"node_modules/react-markdown/lib/react-markdown.js": 403
|
|
||||||
},
|
|
||||||
"usedIds": {
|
|
||||||
"0": 0,
|
|
||||||
"1": 1,
|
|
||||||
"2": 2,
|
|
||||||
"3": 3,
|
|
||||||
"4": 4,
|
|
||||||
"5": 5,
|
|
||||||
"6": 6,
|
|
||||||
"7": 7,
|
|
||||||
"8": 8,
|
|
||||||
"9": 9,
|
|
||||||
"10": 10,
|
|
||||||
"11": 11,
|
|
||||||
"12": 12,
|
|
||||||
"13": 13,
|
|
||||||
"14": 14,
|
|
||||||
"15": 15,
|
|
||||||
"16": 16,
|
|
||||||
"17": 17,
|
|
||||||
"18": 18,
|
|
||||||
"19": 19,
|
|
||||||
"20": 20,
|
|
||||||
"21": 21,
|
|
||||||
"22": 22,
|
|
||||||
"23": 23,
|
|
||||||
"24": 24,
|
|
||||||
"25": 25,
|
|
||||||
"26": 26,
|
|
||||||
"27": 27,
|
|
||||||
"28": 28,
|
|
||||||
"29": 29,
|
|
||||||
"30": 30,
|
|
||||||
"31": 31,
|
|
||||||
"32": 32,
|
|
||||||
"33": 33,
|
|
||||||
"34": 34,
|
|
||||||
"35": 35,
|
|
||||||
"36": 36,
|
|
||||||
"37": 37,
|
|
||||||
"38": 38,
|
|
||||||
"39": 39,
|
|
||||||
"40": 40,
|
|
||||||
"41": 41,
|
|
||||||
"42": 42,
|
|
||||||
"43": 43,
|
|
||||||
"44": 44,
|
|
||||||
"45": 45,
|
|
||||||
"46": 46,
|
|
||||||
"47": 47,
|
|
||||||
"48": 48,
|
|
||||||
"49": 49,
|
|
||||||
"50": 50,
|
|
||||||
"51": 51,
|
|
||||||
"52": 52,
|
|
||||||
"53": 53,
|
|
||||||
"54": 54,
|
|
||||||
"55": 55,
|
|
||||||
"56": 56,
|
|
||||||
"57": 57,
|
|
||||||
"58": 58,
|
|
||||||
"59": 59,
|
|
||||||
"60": 60,
|
|
||||||
"61": 61,
|
|
||||||
"62": 62,
|
|
||||||
"63": 63,
|
|
||||||
"64": 64,
|
|
||||||
"65": 65,
|
|
||||||
"66": 66,
|
|
||||||
"67": 67,
|
|
||||||
"68": 68,
|
|
||||||
"69": 69,
|
|
||||||
"70": 70,
|
|
||||||
"71": 71,
|
|
||||||
"72": 72,
|
|
||||||
"73": 73,
|
|
||||||
"74": 74,
|
|
||||||
"75": 75,
|
|
||||||
"76": 76,
|
|
||||||
"77": 77,
|
|
||||||
"78": 78,
|
|
||||||
"79": 79,
|
|
||||||
"80": 80,
|
|
||||||
"81": 81,
|
|
||||||
"82": 82,
|
|
||||||
"83": 83,
|
|
||||||
"84": 84,
|
|
||||||
"85": 85,
|
|
||||||
"86": 86,
|
|
||||||
"87": 87,
|
|
||||||
"88": 88,
|
|
||||||
"89": 89,
|
|
||||||
"90": 90,
|
|
||||||
"91": 91,
|
|
||||||
"92": 92,
|
|
||||||
"93": 93,
|
|
||||||
"94": 94,
|
|
||||||
"95": 95,
|
|
||||||
"96": 96,
|
|
||||||
"97": 97,
|
|
||||||
"98": 98,
|
|
||||||
"99": 99,
|
|
||||||
"100": 100,
|
|
||||||
"101": 101,
|
|
||||||
"102": 102,
|
|
||||||
"103": 103,
|
|
||||||
"104": 104,
|
|
||||||
"105": 105,
|
|
||||||
"106": 106,
|
|
||||||
"107": 107,
|
|
||||||
"108": 108,
|
|
||||||
"109": 109,
|
|
||||||
"110": 110,
|
|
||||||
"111": 111,
|
|
||||||
"112": 112,
|
|
||||||
"113": 113,
|
|
||||||
"114": 114,
|
|
||||||
"115": 115,
|
|
||||||
"116": 116,
|
|
||||||
"117": 117,
|
|
||||||
"118": 118,
|
|
||||||
"119": 119,
|
|
||||||
"120": 120,
|
|
||||||
"121": 121,
|
|
||||||
"122": 122,
|
|
||||||
"123": 123,
|
|
||||||
"124": 124,
|
|
||||||
"125": 125,
|
|
||||||
"126": 126,
|
|
||||||
"127": 127,
|
|
||||||
"128": 128,
|
|
||||||
"129": 129,
|
|
||||||
"130": 130,
|
|
||||||
"131": 131,
|
|
||||||
"132": 132,
|
|
||||||
"133": 133,
|
|
||||||
"134": 134,
|
|
||||||
"135": 135,
|
|
||||||
"136": 136,
|
|
||||||
"137": 137,
|
|
||||||
"138": 138,
|
|
||||||
"139": 139,
|
|
||||||
"140": 140,
|
|
||||||
"141": 141,
|
|
||||||
"142": 142,
|
|
||||||
"143": 143,
|
|
||||||
"144": 144,
|
|
||||||
"145": 145,
|
|
||||||
"146": 146,
|
|
||||||
"147": 147,
|
|
||||||
"148": 148,
|
|
||||||
"149": 149,
|
|
||||||
"150": 150,
|
|
||||||
"151": 151,
|
|
||||||
"152": 152,
|
|
||||||
"153": 153,
|
|
||||||
"154": 154,
|
|
||||||
"155": 155,
|
|
||||||
"156": 156,
|
|
||||||
"157": 157,
|
|
||||||
"158": 158,
|
|
||||||
"159": 159,
|
|
||||||
"160": 160,
|
|
||||||
"161": 161,
|
|
||||||
"162": 162,
|
|
||||||
"163": 163,
|
|
||||||
"164": 164,
|
|
||||||
"165": 165,
|
|
||||||
"166": 166,
|
|
||||||
"167": 167,
|
|
||||||
"168": 168,
|
|
||||||
"169": 169,
|
|
||||||
"170": 170,
|
|
||||||
"171": 171,
|
|
||||||
"172": 172,
|
|
||||||
"173": 173,
|
|
||||||
"174": 174,
|
|
||||||
"175": 175,
|
|
||||||
"176": 176,
|
|
||||||
"177": 177,
|
|
||||||
"178": 178,
|
|
||||||
"179": 179,
|
|
||||||
"180": 180,
|
|
||||||
"181": 181,
|
|
||||||
"182": 182,
|
|
||||||
"183": 183,
|
|
||||||
"184": 184,
|
|
||||||
"185": 185,
|
|
||||||
"186": 186,
|
|
||||||
"187": 187,
|
|
||||||
"188": 188,
|
|
||||||
"189": 189,
|
|
||||||
"190": 190,
|
|
||||||
"191": 191,
|
|
||||||
"192": 192,
|
|
||||||
"193": 193,
|
|
||||||
"194": 194,
|
|
||||||
"195": 195,
|
|
||||||
"196": 196,
|
|
||||||
"197": 197,
|
|
||||||
"198": 198,
|
|
||||||
"199": 199,
|
|
||||||
"200": 200,
|
|
||||||
"201": 201,
|
|
||||||
"202": 202,
|
|
||||||
"203": 203,
|
|
||||||
"204": 204,
|
|
||||||
"205": 205,
|
|
||||||
"206": 206,
|
|
||||||
"207": 207,
|
|
||||||
"208": 208,
|
|
||||||
"209": 209,
|
|
||||||
"210": 210,
|
|
||||||
"211": 211,
|
|
||||||
"212": 212,
|
|
||||||
"213": 213,
|
|
||||||
"214": 214,
|
|
||||||
"215": 215,
|
|
||||||
"216": 216,
|
|
||||||
"217": 217,
|
|
||||||
"218": 218,
|
|
||||||
"219": 219,
|
|
||||||
"220": 220,
|
|
||||||
"221": 221,
|
|
||||||
"222": 222,
|
|
||||||
"223": 223,
|
|
||||||
"224": 224,
|
|
||||||
"225": 225,
|
|
||||||
"226": 226,
|
|
||||||
"227": 227,
|
|
||||||
"228": 228,
|
|
||||||
"229": 229,
|
|
||||||
"230": 230,
|
|
||||||
"231": 231,
|
|
||||||
"232": 232,
|
|
||||||
"233": 233,
|
|
||||||
"234": 234,
|
|
||||||
"235": 235,
|
|
||||||
"236": 236,
|
|
||||||
"237": 237,
|
|
||||||
"238": 238,
|
|
||||||
"239": 239,
|
|
||||||
"240": 240,
|
|
||||||
"241": 241,
|
|
||||||
"242": 242,
|
|
||||||
"243": 243,
|
|
||||||
"244": 244,
|
|
||||||
"245": 245,
|
|
||||||
"246": 246,
|
|
||||||
"247": 247,
|
|
||||||
"248": 248,
|
|
||||||
"249": 249,
|
|
||||||
"250": 250,
|
|
||||||
"251": 251,
|
|
||||||
"252": 252,
|
|
||||||
"253": 253,
|
|
||||||
"254": 254,
|
|
||||||
"255": 255,
|
|
||||||
"256": 256,
|
|
||||||
"257": 257,
|
|
||||||
"258": 258,
|
|
||||||
"259": 259,
|
|
||||||
"260": 260,
|
|
||||||
"261": 261,
|
|
||||||
"262": 262,
|
|
||||||
"263": 263,
|
|
||||||
"264": 264,
|
|
||||||
"265": 265,
|
|
||||||
"266": 266,
|
|
||||||
"267": 267,
|
|
||||||
"268": 268,
|
|
||||||
"269": 269,
|
|
||||||
"270": 270,
|
|
||||||
"271": 271,
|
|
||||||
"272": 272,
|
|
||||||
"273": 273,
|
|
||||||
"274": 274,
|
|
||||||
"275": 275,
|
|
||||||
"276": 276,
|
|
||||||
"277": 277,
|
|
||||||
"278": 278,
|
|
||||||
"279": 279,
|
|
||||||
"280": 280,
|
|
||||||
"281": 281,
|
|
||||||
"282": 282,
|
|
||||||
"283": 283,
|
|
||||||
"284": 284,
|
|
||||||
"285": 285,
|
|
||||||
"286": 286,
|
|
||||||
"287": 287,
|
|
||||||
"288": 288,
|
|
||||||
"289": 289,
|
|
||||||
"290": 290,
|
|
||||||
"291": 291,
|
|
||||||
"292": 292,
|
|
||||||
"293": 293,
|
|
||||||
"294": 294,
|
|
||||||
"295": 295,
|
|
||||||
"296": 296,
|
|
||||||
"297": 297,
|
|
||||||
"298": 298,
|
|
||||||
"299": 299,
|
|
||||||
"300": 300,
|
|
||||||
"301": 301,
|
|
||||||
"302": 302,
|
|
||||||
"303": 303,
|
|
||||||
"304": 304,
|
|
||||||
"305": 305,
|
|
||||||
"306": 306,
|
|
||||||
"307": 307,
|
|
||||||
"308": 308,
|
|
||||||
"309": 309,
|
|
||||||
"310": 310,
|
|
||||||
"311": 311,
|
|
||||||
"312": 312,
|
|
||||||
"313": 313,
|
|
||||||
"314": 314,
|
|
||||||
"315": 315,
|
|
||||||
"316": 316,
|
|
||||||
"317": 317,
|
|
||||||
"318": 318,
|
|
||||||
"319": 319,
|
|
||||||
"320": 320,
|
|
||||||
"321": 321,
|
|
||||||
"322": 322,
|
|
||||||
"323": 323,
|
|
||||||
"324": 324,
|
|
||||||
"325": 325,
|
|
||||||
"326": 326,
|
|
||||||
"327": 327,
|
|
||||||
"328": 328,
|
|
||||||
"329": 329,
|
|
||||||
"330": 330,
|
|
||||||
"331": 331,
|
|
||||||
"332": 332,
|
|
||||||
"333": 333,
|
|
||||||
"334": 334,
|
|
||||||
"335": 335,
|
|
||||||
"336": 336,
|
|
||||||
"337": 337,
|
|
||||||
"338": 338,
|
|
||||||
"339": 339,
|
|
||||||
"340": 340,
|
|
||||||
"341": 341,
|
|
||||||
"342": 342,
|
|
||||||
"343": 343,
|
|
||||||
"344": 344,
|
|
||||||
"345": 345,
|
|
||||||
"346": 346,
|
|
||||||
"347": 347,
|
|
||||||
"348": 348,
|
|
||||||
"349": 349,
|
|
||||||
"350": 350,
|
|
||||||
"351": 351,
|
|
||||||
"352": 352,
|
|
||||||
"353": 353,
|
|
||||||
"354": 354,
|
|
||||||
"355": 355,
|
|
||||||
"356": 356,
|
|
||||||
"357": 357,
|
|
||||||
"358": 358,
|
|
||||||
"359": 359,
|
|
||||||
"360": 360,
|
|
||||||
"361": 361,
|
|
||||||
"362": 362,
|
|
||||||
"363": 363,
|
|
||||||
"364": 364,
|
|
||||||
"365": 365,
|
|
||||||
"366": 366,
|
|
||||||
"367": 367,
|
|
||||||
"368": 368,
|
|
||||||
"369": 369,
|
|
||||||
"370": 370,
|
|
||||||
"371": 371,
|
|
||||||
"372": 372,
|
|
||||||
"373": 373,
|
|
||||||
"374": 374,
|
|
||||||
"375": 375,
|
|
||||||
"376": 376,
|
|
||||||
"377": 377,
|
|
||||||
"378": 378,
|
|
||||||
"379": 379,
|
|
||||||
"380": 380,
|
|
||||||
"381": 381,
|
|
||||||
"382": 382,
|
|
||||||
"383": 383,
|
|
||||||
"384": 384,
|
|
||||||
"385": 385,
|
|
||||||
"386": 386,
|
|
||||||
"387": 387,
|
|
||||||
"388": 388,
|
|
||||||
"389": 389,
|
|
||||||
"390": 390,
|
|
||||||
"391": 391,
|
|
||||||
"392": 392,
|
|
||||||
"393": 393,
|
|
||||||
"394": 394,
|
|
||||||
"395": 395,
|
|
||||||
"396": 396,
|
|
||||||
"397": 397,
|
|
||||||
"398": 398,
|
|
||||||
"399": 399,
|
|
||||||
"400": 400,
|
|
||||||
"401": 401,
|
|
||||||
"402": 402,
|
|
||||||
"403": 403
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chunks": {
|
|
||||||
"byName": {
|
|
||||||
"commons": 0,
|
|
||||||
"static/runtime/webpack.js": 1,
|
|
||||||
"static/runtime/main.js": 2,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/_app.js": 3,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/edit.js": 4,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/index.js": 5,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/k.js": 6,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/new.js": 7,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/settings.js": 8,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/_error.js": 9
|
|
||||||
},
|
|
||||||
"bySource": {
|
|
||||||
"0 node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/MngDoc.js ../components/CodeMirror": 10,
|
|
||||||
"0 node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Markdown.js react-markdown": 11
|
|
||||||
},
|
|
||||||
"usedIds": [
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
11,
|
|
||||||
2,
|
|
||||||
3,
|
|
||||||
4,
|
|
||||||
5,
|
|
||||||
6,
|
|
||||||
7,
|
|
||||||
8,
|
|
||||||
9
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"/_app":"static/MdYEkay1bViLxxtsssdFt/pages/_app.js","/_document":"static/MdYEkay1bViLxxtsssdFt/pages/_document.js","/edit":"static/MdYEkay1bViLxxtsssdFt/pages/edit.js","/index":"static/MdYEkay1bViLxxtsssdFt/pages/index.js","/k":"static/MdYEkay1bViLxxtsssdFt/pages/k.js","/new":"static/MdYEkay1bViLxxtsssdFt/pages/new.js","/settings":"static/MdYEkay1bViLxxtsssdFt/pages/settings.js","/_error":"static/MdYEkay1bViLxxtsssdFt/pages/_error.js","/":"static/MdYEkay1bViLxxtsssdFt/pages/index.js"}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
{
|
|
||||||
"modules": {
|
|
||||||
"byIdentifier": {
|
|
||||||
"external \"react\"": 0,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/getUrl.js": 1,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/redux/actions/userAct.js": 2,
|
|
||||||
"external \"glamor\"": 3,
|
|
||||||
"external \"@babel/runtime/regenerator\"": 4,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/theme.js": 5,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/redux/store.js 4e1280ed09dd33013b4dfa4dd70e85b4": 6,
|
|
||||||
"external \"react-redux\"": 7,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/mapUser.js": 8,
|
|
||||||
"external \"isomorphic-unfetch\"": 9,
|
|
||||||
"external \"next/router\"": 10,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/PaddedRow.js": 11,
|
|
||||||
"external \"redux\"": 12,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Spinner.js": 13,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/getJwt.js": 14,
|
|
||||||
"external \"next/link\"": 15,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Page.js 7cd3bec4a0252faf8f91892a2f798527": 16,
|
|
||||||
"external \"url\"": 17,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/keys.js": 18,
|
|
||||||
"external \"url-join\"": 19,
|
|
||||||
"external \"next/dynamic\"": 20,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/updStateFromId.js": 21,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Markdown.js d5b5492a768bba52c33c5b22a90aa8a1": 22,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/checkDirParts.js": 23,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/AddDoc.js": 24,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/MngDoc.js cfa406d5564443825298c8c4c761c7c2": 25,
|
|
||||||
"external \"next/document\"": 26,
|
|
||||||
"external \"next/app\"": 27,
|
|
||||||
"external \"next/head\"": 28,
|
|
||||||
"external \"glamor/server\"": 29,
|
|
||||||
"external \"react-paginate\"": 30,
|
|
||||||
"external \"codemirror\"": 31,
|
|
||||||
"external \"codemirror/mode/markdown/markdown\"": 32,
|
|
||||||
"external \"react-markdown\"": 33,
|
|
||||||
"multi ./pages/_app.js": 34,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/milligram.js": 35,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/Roboto.js": 36,
|
|
||||||
"multi ./pages/_document.js": 37,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/_document.js": 38,
|
|
||||||
"multi ./pages/edit.js": 39,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/edit.js": 40,
|
|
||||||
"multi ./pages/index.js": 41,
|
|
||||||
"multi ./pages/k.js": 42,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/k.js": 43,
|
|
||||||
"multi ./pages/new.js": 44,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/new.js": 45,
|
|
||||||
"multi ./pages/settings.js": 46,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/settings.js": 47,
|
|
||||||
"multi node_modules/next/dist/pages/_error.js": 48,
|
|
||||||
"node_modules/next/dist/pages/_error.js": 49,
|
|
||||||
"external \"next/error\"": 50,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/index.js 63f90c0b7addefbc51a5877f453fdcbd": 51,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/_app.js 1362119e6d3d895366421ac2b14cb639": 52,
|
|
||||||
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/CodeMirror.js": 53
|
|
||||||
},
|
|
||||||
"usedIds": {
|
|
||||||
"0": 0,
|
|
||||||
"1": 1,
|
|
||||||
"2": 2,
|
|
||||||
"3": 3,
|
|
||||||
"4": 4,
|
|
||||||
"5": 5,
|
|
||||||
"6": 6,
|
|
||||||
"7": 7,
|
|
||||||
"8": 8,
|
|
||||||
"9": 9,
|
|
||||||
"10": 10,
|
|
||||||
"11": 11,
|
|
||||||
"12": 12,
|
|
||||||
"13": 13,
|
|
||||||
"14": 14,
|
|
||||||
"15": 15,
|
|
||||||
"16": 16,
|
|
||||||
"17": 17,
|
|
||||||
"18": 18,
|
|
||||||
"19": 19,
|
|
||||||
"20": 20,
|
|
||||||
"21": 21,
|
|
||||||
"22": 22,
|
|
||||||
"23": 23,
|
|
||||||
"24": 24,
|
|
||||||
"25": 25,
|
|
||||||
"26": 26,
|
|
||||||
"27": 27,
|
|
||||||
"28": 28,
|
|
||||||
"29": 29,
|
|
||||||
"30": 30,
|
|
||||||
"31": 31,
|
|
||||||
"32": 32,
|
|
||||||
"33": 33,
|
|
||||||
"34": 34,
|
|
||||||
"35": 35,
|
|
||||||
"36": 36,
|
|
||||||
"37": 37,
|
|
||||||
"38": 38,
|
|
||||||
"39": 39,
|
|
||||||
"40": 40,
|
|
||||||
"41": 41,
|
|
||||||
"42": 42,
|
|
||||||
"43": 43,
|
|
||||||
"44": 44,
|
|
||||||
"45": 45,
|
|
||||||
"46": 46,
|
|
||||||
"47": 47,
|
|
||||||
"48": 48,
|
|
||||||
"49": 49,
|
|
||||||
"50": 50,
|
|
||||||
"51": 51,
|
|
||||||
"52": 52,
|
|
||||||
"53": 53
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chunks": {
|
|
||||||
"byName": {
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/_app.js": 0,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/_document.js": 1,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/edit.js": 2,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/index.js": 3,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/k.js": 4,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/new.js": 5,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/settings.js": 6,
|
|
||||||
"static/MdYEkay1bViLxxtsssdFt/pages/_error.js": 7
|
|
||||||
},
|
|
||||||
"bySource": {
|
|
||||||
"0 node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/MngDoc.js ../components/CodeMirror": 8
|
|
||||||
},
|
|
||||||
"usedIds": [
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
3,
|
|
||||||
4,
|
|
||||||
5,
|
|
||||||
6,
|
|
||||||
7,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
/* This cache is used by webpack for instantiated modules */
|
|
||||||
module.exports = {}
|
|
||||||
|
|
||||||
17
Dockerfile
17
Dockerfile
@@ -1,23 +1,30 @@
|
|||||||
FROM node:8-alpine
|
FROM node:8-alpine
|
||||||
|
|
||||||
RUN apk add yarn git bash s6
|
# install build stuff ( make python2 g++ ) for bcrypt since
|
||||||
|
# alpine linux doesn't have pre-built bcrypt
|
||||||
|
RUN apk add git bash s6 make python2 g++
|
||||||
RUN mkdir -p /opt/mykb
|
RUN mkdir -p /opt/mykb
|
||||||
|
|
||||||
# install node_modules to tmp so it can be cached
|
# install node_modules to tmp so it can be cached
|
||||||
RUN mkdir -p /tmp/mykb
|
RUN mkdir -p /tmp/mykb
|
||||||
COPY package.json /tmp/mykb
|
COPY package.json /tmp/mykb
|
||||||
RUN cd /tmp/mykb && yarn
|
RUN cd /tmp/mykb && npm install
|
||||||
RUN mv /tmp/mykb/node_modules /opt/mykb/
|
RUN mv /tmp/mykb/node_modules /opt/mykb/
|
||||||
|
|
||||||
COPY . /opt/mykb
|
COPY . /opt/mykb
|
||||||
RUN cd /opt/mykb && yarn build
|
RUN cd /opt/mykb && npm run build
|
||||||
|
RUN cd /opt/mykb && npm prune --production
|
||||||
|
RUN npm cache clean --force
|
||||||
|
|
||||||
|
# remove packages from building bcrypt
|
||||||
|
RUN apk del make python2 g++
|
||||||
|
|
||||||
COPY docker_startup.sh /mykb
|
COPY docker_startup.sh /mykb
|
||||||
RUN chmod +x /mykb
|
RUN chmod +x /mykb
|
||||||
|
|
||||||
VOLUME /kb /db /config
|
VOLUME /kb /config
|
||||||
|
|
||||||
EXPOSE 3030
|
EXPOSE 3000
|
||||||
|
|
||||||
ARG GIT_NAME=mykb
|
ARG GIT_NAME=mykb
|
||||||
ARG GIT_EMAIL=mykb@localhost
|
ARG GIT_EMAIL=mykb@localhost
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -7,67 +7,63 @@
|
|||||||
You can try the demo at: https://mykb.jjsweb.site
|
You can try the demo at: https://mykb.jjsweb.site
|
||||||
|
|
||||||
\- Login
|
\- Login
|
||||||
email: notadmin
|
email: admin
|
||||||
password: secretpass
|
password: secretpass
|
||||||
|
|
||||||
P.S. the demo is reset every 10 minutes
|
P.S. the demo is reset every 10 minutes
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
MYKB is a simple file system/markdown based knowledge base editor/viewer built with [Feathers](http://feathersjs.com) and [Next.js](https://github.com/zeit/next.js)
|
MYKB is a file system/markdown based knowledge base editor/viewer built with [Next.js](https://github.com/zeit/next.js)
|
||||||
|
|
||||||
Current features:
|
Current features:
|
||||||
|
|
||||||
- live preview when editing a doc
|
- live preview when editing a doc
|
||||||
- live file system watching
|
- live file system watching
|
||||||
- caching of docs to speed up searching/viewing of docs
|
- caching of docs to speed up searching/viewing of docs
|
||||||
|
- offline viewing of cached docs (requires browser that supports service workers)
|
||||||
- automatic git versioning
|
- automatic git versioning
|
||||||
- automatic trusting of cloudflare reverse proxies
|
- automatic trusting of Cloudflare reverse proxies
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
Getting up and running is as easy as 1, 2, 3
|
- With Docker
|
||||||
|
```
|
||||||
|
docker run --name mykb -v /path/to/docs:/kb -v /path/to/config:/config --env "PUID=USER_ID" --env "PGID=GROUP_ID" -p 3000:3000 ijjk/mykb:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
- With yarn (or npm)
|
||||||
|
|
||||||
1. Clone repo
|
1. Clone repo
|
||||||
```
|
```
|
||||||
git clone https://github.com/ijjk/mykb
|
git clone https://github.com/ijjk/mykb
|
||||||
```
|
```
|
||||||
2. Install dependencies (omit `--prod` if developing)
|
2. Install dependencies
|
||||||
```
|
```
|
||||||
cd path/to/mykb; npm i --prod
|
cd path/to/mykb; yarn
|
||||||
```
|
```
|
||||||
3. Start it
|
3. Build it
|
||||||
```
|
```
|
||||||
npm start
|
yarn build && NODE_ENV=production node ./bin/genSecret.js
|
||||||
|
```
|
||||||
|
4. Start it
|
||||||
|
```
|
||||||
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
host.json
|
|
||||||
|
|
||||||
| Name | Description |
|
|
||||||
| ---- | ----------- |
|
|
||||||
| host | The host to listen on |
|
|
||||||
| port | The port to listen on |
|
|
||||||
| pathPrefix | Used to prefix all urls for reverse proxies |
|
|
||||||
|
|
||||||
production.json (overrides default.json with production NODE_ENV var)
|
production.json (overrides default.json with production NODE_ENV var)
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ---- | ----------- |
|
| ---- | ----------- |
|
||||||
| useGit | Whether or not to use a git repo to automatically version changes to docs (requires git to be installed) |
|
| useGit | Whether or not to use a git repo to automatically version changes to docs (requires git to be installed) |
|
||||||
| docsDir | The directory where the markdown docs are located |
|
| docsDir | The directory where the markdown docs are located |
|
||||||
| cacheSize | Max size of docs to store in memory for faster searching (default 7.5MB) |
|
| cacheSize | Max size of docs to store in memory for faster searching (default 10MB) |
|
||||||
| trustCloudflare | Whether to trust X-Forwarded-For header from cloudflare IPs (used for rate limiting) |
|
| trustCloudflare | Whether to trust X-Forwarded-For header from Cloudflare IPs (used for rate limiting) |
|
||||||
|
|
||||||
If using git the `user.email` and `user.name` configs need to be set either globally or on the docs repo
|
|
||||||
|
|
||||||
trustIPs.json - An array of [proxy-addr](https://www.npmjs.com/package/proxy-addr) compatible addresses to trust the X-Forwarded-For header from (Only needed if behind reverse proxy)
|
trustIPs.json - An array of [proxy-addr](https://www.npmjs.com/package/proxy-addr) compatible addresses to trust the X-Forwarded-For header from (Only needed if behind reverse proxy)
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Simply run `npm test` and all your tests in the `test/` directory will be run
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2017
|
Copyright (c) 2017
|
||||||
|
|||||||
35
bin/genSecret.js
Executable file
35
bin/genSecret.js
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#! /usr/bin/env node
|
||||||
|
const path = require('path')
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const DB = require('../src/server/util/db')
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV !== 'production'
|
||||||
|
const configFile = (isDev ? 'default' : 'production') + '.json'
|
||||||
|
const configPath = path.join(__dirname, '../config', configFile)
|
||||||
|
|
||||||
|
const config = new DB(configPath)
|
||||||
|
|
||||||
|
config.loading
|
||||||
|
.then(() => {
|
||||||
|
const { secret } = config.data
|
||||||
|
if (!isDev && secret && !process.argv.some(arg => arg === '-f')) {
|
||||||
|
return console.log(
|
||||||
|
'Secret already exists, not updating. Use -f to force update'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
config
|
||||||
|
.setData({
|
||||||
|
...config.data,
|
||||||
|
secret: crypto.randomBytes(256).toString('hex'),
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log(
|
||||||
|
`Authentication secret successfully updated in ${configPath}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
29
bin/genServiceWorker.js
Normal file
29
bin/genServiceWorker.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
const terser = require('terser')
|
||||||
|
const swPrecache = require('sw-precache')
|
||||||
|
const outputPath = path.resolve('public/sw.js')
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let sw = await fs.readFile(path.resolve('src/client/util/offlineFallback.js'))
|
||||||
|
sw = sw.toString()
|
||||||
|
|
||||||
|
sw += await swPrecache.generate({
|
||||||
|
cacheId: 'mykb-cache',
|
||||||
|
stripPrefixMulti: {
|
||||||
|
[path.resolve('public/')]: '',
|
||||||
|
[path.resolve('.next/static/')]: '/_next/static',
|
||||||
|
},
|
||||||
|
staticFileGlobs: ['public/!(sw.js)', '.next/static/**/*'].map(p =>
|
||||||
|
path.resolve(p)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
const precacheRegex = /var precacheConfig.*]];/
|
||||||
|
const precacheConfig = sw.match(precacheRegex)
|
||||||
|
sw = sw.replace(precacheRegex, '')
|
||||||
|
sw = precacheConfig[0] + sw
|
||||||
|
sw = terser.minify(sw).code
|
||||||
|
|
||||||
|
await fs.outputFile(outputPath, sw)
|
||||||
|
}
|
||||||
|
main()
|
||||||
@@ -1,33 +1,25 @@
|
|||||||
{
|
{
|
||||||
"docsDir": "../kb",
|
"port": 3000,
|
||||||
|
"basePath": "/",
|
||||||
|
"docsDir": "./kb",
|
||||||
|
"cacheSize": 10000000,
|
||||||
"trustCloudflare": false,
|
"trustCloudflare": false,
|
||||||
"cacheSize": 7500000,
|
"maxDocsLimit": 50,
|
||||||
"paginate": {
|
"defDocsLimit": 15,
|
||||||
"default": 10,
|
"useGit": false,
|
||||||
"max": 50
|
"searchDelay": 100,
|
||||||
|
"date": {
|
||||||
|
"options": {
|
||||||
|
"month": "short",
|
||||||
|
"day": "numeric",
|
||||||
|
"year": "numeric"
|
||||||
|
},
|
||||||
|
"locale": "en-US"
|
||||||
},
|
},
|
||||||
"authentication": {
|
|
||||||
"secret": "030c8d13ff1eb57c5f7dfcd604005a034f2b1fcf89ce7898d30fab5908e33185d3eb94d9849a7d5aaac3c888e3a0d93031d32a04830609768abe6744513d2433f8dad73b8aab46f50d7d2cf4b58f20b2766815969db3dfa6653b110f305f17f757119004b862e6df3a3da6f0e1d9b24c98515fbf29729e42cc274ef28f87cdd84e48622f86102e7a046e1c83ed5e0c06921ad3093c1ffadfb8b2c1f9214c934dc3a118ed615d40f180d79c65f3a7ef2e30d10e3260e11be4535eeaabcd2ff16415760811a2a02817acad5701e735c40622f37c7b055d5cab3c7aa0539550c3e20479ceda988fdcd9c743b982f018a1800b8e4f4c08ccaae679f2fe1d798251cf",
|
|
||||||
"strategies": [
|
|
||||||
"jwt",
|
|
||||||
"local"
|
|
||||||
],
|
|
||||||
"path": "/auth",
|
|
||||||
"service": "users",
|
|
||||||
"jwt": {
|
"jwt": {
|
||||||
"header": {
|
"issuer": "mykb",
|
||||||
"typ": "access"
|
"expiresIn": "7d",
|
||||||
|
"audience": "mykb"
|
||||||
},
|
},
|
||||||
"subject": "anonymous",
|
"secret": ""
|
||||||
"issuer": "feathers",
|
|
||||||
"algorithm": "HS256",
|
|
||||||
"expiresIn": "1d"
|
|
||||||
},
|
|
||||||
"local": {
|
|
||||||
"entity": "user",
|
|
||||||
"usernameField": "email",
|
|
||||||
"passwordField": "password"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nedb": "../db"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"host": "localhost",
|
|
||||||
"port": 3030,
|
|
||||||
"pathPrefix": "/"
|
|
||||||
}
|
|
||||||
1
config/users.json
Normal file
1
config/users.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
PKGDIR="/opt/mykb"
|
PKGDIR="/opt/mykb"
|
||||||
DBDIR="$PKGDIR/db"
|
|
||||||
KBDIR="$PKGDIR/kb"
|
KBDIR="$PKGDIR/kb"
|
||||||
CONFDIR="$PKGDIR/config"
|
CONFDIR="$PKGDIR/config"
|
||||||
|
|
||||||
if [ -d "/db" ];then
|
# check if kb volume exists and is empty and copy starter doc if it is
|
||||||
rm -rf $DBDIR
|
if [ -d "/kb" ] && [ -z "$(ls -A /kb)" ] && [ -f "/opt/mykb/kb/hello world.md" ];then
|
||||||
ln -s /db $DBDIR
|
cp "/opt/mykb/kb/hello world.md" /kb/
|
||||||
DBDIR="/db"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d "/kb" ];then
|
if [ -d "/kb" ];then
|
||||||
@@ -29,12 +27,12 @@ export NODE_ENV=production
|
|||||||
|
|
||||||
if [ -z "$PUID" ];then
|
if [ -z "$PUID" ];then
|
||||||
echo 'no PUID set running as default user'
|
echo 'no PUID set running as default user'
|
||||||
node ./genSecret.js && node ./src
|
node ./bin/genSecret.js && node ./src
|
||||||
else
|
else
|
||||||
echo 'chowning files'
|
echo 'chowning files'
|
||||||
DIRS=($KBDIR $DBDIR $CONFDIR)
|
DIRS=($KBDIR $CONFDIR)
|
||||||
for dir in ${DIRS[@]};do chown "$PUID:$PGID" $dir;done
|
for dir in ${DIRS[@]};do chown "$PUID:$PGID" $dir;done
|
||||||
chown "$PUID:$PGID" -R $CONFDIR
|
chown "$PUID:$PGID" -R $CONFDIR
|
||||||
s6-setuidgid "$PUID:$PGID" node ./genSecret.js
|
s6-setuidgid "$PUID:$PGID" node ./bin/genSecret.js
|
||||||
s6-setuidgid "$PUID:$PGID" node ./src
|
s6-setuidgid "$PUID:$PGID" node ./src
|
||||||
fi
|
fi
|
||||||
28
genSecret.js
28
genSecret.js
@@ -1,28 +0,0 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const crypto = require('crypto')
|
|
||||||
const secret = crypto.randomBytes(256).toString('hex')
|
|
||||||
const isProd = process.env.NODE_ENV === 'production'
|
|
||||||
const confFile = (isProd ? 'production' : 'default') + '.json'
|
|
||||||
const config = require('./config/' + confFile)
|
|
||||||
const configPath = path.join(__dirname, 'config', confFile)
|
|
||||||
|
|
||||||
// if in production check if secret exists and -f switch is set
|
|
||||||
if (isProd && config.authentication && config.authentication.secret) {
|
|
||||||
if (!process.argv.some(arg => arg === '-f')) {
|
|
||||||
return console.log(
|
|
||||||
'Secret already exists, not updating. Use -f to force update'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.authentication) {
|
|
||||||
config.authentication = { secret }
|
|
||||||
} else {
|
|
||||||
config.authentication.secret = secret
|
|
||||||
}
|
|
||||||
fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', err => {
|
|
||||||
if (err) return console.error(err)
|
|
||||||
console.log('\nAuthentication secret successfully updated in', confFile)
|
|
||||||
})
|
|
||||||
136
package.json
136
package.json
@@ -1,84 +1,88 @@
|
|||||||
{
|
{
|
||||||
"name": "mykb",
|
"name": "mykb",
|
||||||
"description": "A file system/markdown based knowledge base editor/viewer",
|
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
|
"description": "A file system/markdown based knowledge base editor/viewer",
|
||||||
"main": "src",
|
"main": "src",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"feathers",
|
|
||||||
"react",
|
"react",
|
||||||
"next.js",
|
"react-hooks",
|
||||||
"markdown"
|
"markdown",
|
||||||
|
"express",
|
||||||
|
"next"
|
||||||
],
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node src",
|
||||||
|
"build:clean": "rimraf .next",
|
||||||
|
"build:next": "next build",
|
||||||
|
"build:sw": "node bin/genServiceWorker.js",
|
||||||
|
"build": "run-s build:clean build:next build:sw",
|
||||||
|
"format-e": "eslint --fix .",
|
||||||
|
"format-p": "prettier --ignore-path .eslintignore --write '**/*.js'",
|
||||||
|
"format": "run-s format-p format-e",
|
||||||
|
"start": "cross-env NODE_ENV=production node src"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.js": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"name": "JJ Kasper",
|
"name": "JJ Kasper",
|
||||||
"email": "jj@jjsweb.site"
|
"email": "jj@jjsweb.site"
|
||||||
},
|
},
|
||||||
"contributors": [],
|
"repository": "https://github.com/ijjk/mykb",
|
||||||
"bugs": {},
|
"license": "MIT",
|
||||||
"directories": {
|
"private": false,
|
||||||
"lib": "src",
|
|
||||||
"test": "test/"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8.0.0",
|
|
||||||
"yarn": ">= 0.18.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"format": "prettier --ignore-path .eslintignore --write '**/*.js'",
|
|
||||||
"lint": "eslint . --config .eslintrc.json",
|
|
||||||
"mocha": "cross-env NODE_ENV=production mocha test/ --recursive --exit",
|
|
||||||
"build": "next build",
|
|
||||||
"analyze": "cross-env ANALYZE=true next build",
|
|
||||||
"dev": "nodemon --ignore src/components --ignore src/redux --ignore src/styles --exec 'npm run start:dev'",
|
|
||||||
"start:dev": "node src/",
|
|
||||||
"start": "cross-env NODE_ENV=production node src/",
|
|
||||||
"test": "npm run lint && npm run build && npm run mocha",
|
|
||||||
"postinstall": "node ./genSecret.js; exit 0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@feathersjs/authentication": "^2.1.5",
|
"bcrypt": "^3.0.2",
|
||||||
"@feathersjs/authentication-jwt": "^2.0.0",
|
"body-parser": "^1.18.3",
|
||||||
"@feathersjs/authentication-local": "^1.1.2",
|
"chokidar": "^2.0.4",
|
||||||
"@feathersjs/configuration": "^1.0.2",
|
"codemirror": "^5.41.0",
|
||||||
"@feathersjs/errors": "^3.3.0",
|
"compression": "^1.7.3",
|
||||||
"@feathersjs/express": "^1.2.2",
|
|
||||||
"@feathersjs/feathers": "^3.1.4",
|
|
||||||
"chokidar": "^2.0.3",
|
|
||||||
"codemirror": "^5.37.0",
|
|
||||||
"compression": "^1.7.2",
|
|
||||||
"cookie-parser": "^1.4.3",
|
"cookie-parser": "^1.4.3",
|
||||||
"cors": "^2.8.4",
|
"cross-env": "^5.2.0",
|
||||||
"cross-env": "^5.1.4",
|
"express": "^4.16.4",
|
||||||
"express-rate-limit": "^2.11.0",
|
"express-rate-limit": "^3.3.2",
|
||||||
"feathers-nedb": "^3.0.0",
|
"fs-extra": "^7.0.1",
|
||||||
"fs-extra": "^5.0.0",
|
"helmet": "^3.15.0",
|
||||||
"glamor": "^2.20.40",
|
"isomorphic-unfetch": "^3.0.0",
|
||||||
"glob": "^7.1.2",
|
"jsonwebtoken": "^8.3.0",
|
||||||
"helmet": "^3.12.0",
|
"keymirror": "^0.1.1",
|
||||||
"if-env": "^1.0.4",
|
"next": "^7.0.2",
|
||||||
"isomorphic-unfetch": "^2.0.0",
|
"passport": "^0.4.0",
|
||||||
"milligram": "^1.3.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"nedb": "^1.8.0",
|
"passport-local": "^1.0.0",
|
||||||
"next": "^7.0.0",
|
"react": "^16.7.0-alpha.0",
|
||||||
"react": "^16.3.2",
|
"react-dom": "^16.7.0-alpha.0",
|
||||||
"react-dom": "^16.3.2",
|
"react-markdown": "^4.0.3",
|
||||||
"react-markdown": "^3.3.0",
|
"react-paginate": "^6.0.0",
|
||||||
"react-paginate": "^5.2.3",
|
"react-redux": "^5.1.0",
|
||||||
"react-redux": "^5.0.7",
|
"redux": "^4.0.1",
|
||||||
"redux": "^4.0.0",
|
"redux-localstorage": "^0.4.1",
|
||||||
"simple-git": "^1.92.0",
|
"simple-git": "^1.107.0",
|
||||||
"url-join": "^4.0.0",
|
"url-join": "^4.0.0",
|
||||||
"winston": "^2.4.1"
|
"uuid": "^3.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "^8.2.3",
|
"babel-eslint": "^10.0.1",
|
||||||
"eslint": "^4.19.1",
|
"eslint": "^5.8.0",
|
||||||
"eslint-plugin-react": "^7.8.2",
|
"eslint-plugin-react": "^7.11.1",
|
||||||
"mocha": "^5.1.1",
|
"husky": "^1.1.3",
|
||||||
"nodemon": "^1.17.3",
|
"lint-staged": "^8.0.4",
|
||||||
"prettier": "^1.13.4",
|
"npm-run-all": "^4.1.3",
|
||||||
"redux-logger": "^3.0.6",
|
"prettier": "^1.15.1",
|
||||||
"request": "^2.85.0",
|
"rimraf": "^2.6.2",
|
||||||
"request-promise": "^4.2.2"
|
"server-destroy": "^1.0.1",
|
||||||
|
"sw-precache": "^5.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,52 @@
|
|||||||
import Head from 'next/head'
|
|
||||||
import '../src/styles/global'
|
|
||||||
import store from '../src/redux/store'
|
|
||||||
import { Provider } from 'react-redux'
|
|
||||||
import App, { Container } from 'next/app'
|
import App, { Container } from 'next/app'
|
||||||
import { setUser, doLogin } from '../src/redux/actions/userAct'
|
import Head from 'next/head'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import withRedux from '../src/client/hocs/withRedux'
|
||||||
|
import Header from '../src/client/layout/header'
|
||||||
|
import Footer from '../src/client/layout/footer'
|
||||||
|
import Shortcuts from '../src/client/comps/shortcuts'
|
||||||
|
import Milligram from '../src/client/styles/milligram'
|
||||||
|
import Roboto from '../src/client/styles/roboto'
|
||||||
|
import Global from '../src/client/styles/global'
|
||||||
|
import '../src/client/util/registerServiceWorker'
|
||||||
|
|
||||||
const ssr = typeof window === 'undefined'
|
class MyApp extends App {
|
||||||
|
|
||||||
export default class MyApp extends App {
|
|
||||||
static async getInitialProps({ Component, ctx }) {
|
static async getInitialProps({ Component, ctx }) {
|
||||||
let user = {}
|
|
||||||
let setup = false
|
|
||||||
if (ssr) {
|
|
||||||
user = ctx.req.user || {}
|
|
||||||
setup = ctx.req.doSetup || false
|
|
||||||
}
|
|
||||||
let pageProps = {}
|
let pageProps = {}
|
||||||
if (Component.getInitialProps) {
|
if (Component.getInitialProps) {
|
||||||
pageProps = await Component.getInitialProps(ctx)
|
pageProps = await Component.getInitialProps(ctx)
|
||||||
}
|
}
|
||||||
return { Component, pageProps, user, setup }
|
return { Component, pageProps }
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
const { user, setup } = this.props
|
|
||||||
setUser({ ...user, setup })
|
|
||||||
if (!ssr && !user.email) {
|
|
||||||
const { jwt } = window.localStorage
|
|
||||||
if (jwt) doLogin(null, jwt, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { Component, pageProps } = this.props
|
const { Component, pageProps, reduxStore } = this.props
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={reduxStore}>
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>My Knowledge Base</title>
|
<title>My Knowledge Base</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<div className="fill main">
|
||||||
<Container>
|
<Container>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</Container>
|
</Container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
<Shortcuts />
|
||||||
|
|
||||||
|
{/* style components */}
|
||||||
|
<Roboto />
|
||||||
|
<Milligram />
|
||||||
|
<Global />
|
||||||
</>
|
</>
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withRedux(MyApp)
|
||||||
|
|||||||
@@ -1,27 +1,13 @@
|
|||||||
import getUrl from '../src/util/getUrl'
|
|
||||||
import { renderStaticOptimized } from 'glamor/server'
|
|
||||||
import Document, { Head, Main, NextScript } from 'next/document'
|
import Document, { Head, Main, NextScript } from 'next/document'
|
||||||
|
import addBase from '../src/util/addBase'
|
||||||
|
|
||||||
export default class MyDocument extends Document {
|
export default class MyDocument extends Document {
|
||||||
static async getInitialProps({ renderPage }) {
|
|
||||||
const page = renderPage()
|
|
||||||
const styles = renderStaticOptimized(() => page.html || page.errorHtml)
|
|
||||||
return { ...page, ...styles }
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
const { __NEXT_DATA__, ids } = props
|
|
||||||
if (ids) {
|
|
||||||
__NEXT_DATA__.ids = this.props.ids
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<html>
|
<html>
|
||||||
<Head>
|
<Head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
|
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||||
@@ -29,35 +15,36 @@ export default class MyDocument extends Document {
|
|||||||
<link
|
<link
|
||||||
rel="apple-touch-icon"
|
rel="apple-touch-icon"
|
||||||
sizes="180x180"
|
sizes="180x180"
|
||||||
href={getUrl('/apple-touch-icon.png')}
|
href={addBase('/apple-touch-icon.png')}
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
type="image/png"
|
type="image/png"
|
||||||
sizes="32x32"
|
sizes="32x32"
|
||||||
href={getUrl('/favicon-32x32.png')}
|
href={addBase('/favicon-32x32.png')}
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
type="image/png"
|
type="image/png"
|
||||||
sizes="16x16"
|
sizes="16x16"
|
||||||
href={getUrl('/favicon-16x16.png')}
|
href={addBase('/favicon-16x16.png')}
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href={getUrl('/site.webmanifest')} />
|
<link rel="manifest" href={addBase('/site.webmanifest')} />
|
||||||
<link
|
<link
|
||||||
rel="mask-icon"
|
rel="mask-icon"
|
||||||
href={getUrl('/safari-pinned-tab.svg')}
|
href={addBase('/safari-pinned-tab.svg')}
|
||||||
color="#00d1b2"
|
color="#00d1b2"
|
||||||
/>
|
/>
|
||||||
<meta name="msapplication-TileColor" content="#202225" />
|
<meta name="msapplication-TileColor" content="#202225" />
|
||||||
<meta name="theme-color" content="#202225" />
|
<meta name="theme-color" content="#202225" />
|
||||||
<style
|
|
||||||
dangerouslySetInnerHTML={{ __html: this.props.css }}
|
|
||||||
data-glamor
|
|
||||||
/>
|
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: 'window.kbConf=' + JSON.stringify(app.get('kbConf')),
|
__html:
|
||||||
|
'window.publicConfig=' +
|
||||||
|
JSON.stringify({
|
||||||
|
...global.publicConfig,
|
||||||
|
ssr: false,
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
|||||||
39
pages/_error.js
Normal file
39
pages/_error.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default class Error extends React.Component {
|
||||||
|
static getInitialProps({ res, err }) {
|
||||||
|
let statusCode = null
|
||||||
|
if (res) statusCode = res.statusCode
|
||||||
|
else if (err) statusCode = err.statusCode
|
||||||
|
return { statusCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { statusCode } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fill">
|
||||||
|
<h4>
|
||||||
|
{(() => {
|
||||||
|
if (statusCode === 404) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span>404</span> | This page could not be found
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return statusCode ? `Error: ${statusCode}` : 'An error occurred...'
|
||||||
|
})()}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
51
pages/doc.js
Normal file
51
pages/doc.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import addBase from '../src/util/addBase'
|
||||||
|
import loadDocs from '../src/client/util/loadDocs'
|
||||||
|
import { deleteDoc } from '../src/client/util/docHelpers'
|
||||||
|
import RequireUser from '../src/client/comps/requireUser'
|
||||||
|
|
||||||
|
const Markdown = dynamic(() => import('react-markdown'))
|
||||||
|
|
||||||
|
function Doc({ cache, query }) {
|
||||||
|
return (
|
||||||
|
<RequireUser>
|
||||||
|
<div className="container fill padded">
|
||||||
|
{(() => {
|
||||||
|
const { id, md } = cache[query.id] || {}
|
||||||
|
|
||||||
|
if (!id) return <p>Doc was not found...</p>
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h5>
|
||||||
|
{`${id} - `}
|
||||||
|
<Link
|
||||||
|
href={{ pathname: '/edit', query }}
|
||||||
|
as={{ pathname: addBase('/edit'), query }}
|
||||||
|
>
|
||||||
|
<a id="edit">edit</a>
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
className="float-right"
|
||||||
|
onClick={() => deleteDoc(query.id)}
|
||||||
|
>
|
||||||
|
DELETE
|
||||||
|
</button>
|
||||||
|
</h5>
|
||||||
|
<Markdown source={md} className="Markdown" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</RequireUser>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Doc.getInitialProps = async ({ query }) => {
|
||||||
|
await loadDocs(query, true)
|
||||||
|
return { query }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(({ cache }) => ({ cache }))(Doc)
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import React, { Component } from 'react'
|
import React from 'react'
|
||||||
import Page from '../src/components/Page'
|
import EditDoc from '../src/client/comps/editDoc'
|
||||||
import MngDoc from '../src/components/MngDoc'
|
import loadDocs from '../src/client/util/loadDocs'
|
||||||
import AddDoc from '../src/components/AddDoc'
|
import RequireUser from '../src/client/comps/requireUser'
|
||||||
|
|
||||||
class Edit extends Component {
|
function Edit({ query }) {
|
||||||
render() {
|
|
||||||
const { found, doc } = this.props
|
|
||||||
if (!found)
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<RequireUser>
|
||||||
<h3>Doc not found...</h3>
|
<EditDoc {...{ query }} />
|
||||||
</Page>
|
</RequireUser>
|
||||||
)
|
)
|
||||||
return <MngDoc {...{ doc }} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Edit.getInitialProps = async ({ query }) => {
|
||||||
|
await loadDocs(query, true)
|
||||||
|
return { query }
|
||||||
}
|
}
|
||||||
export default AddDoc(Edit)
|
|
||||||
|
export default Edit
|
||||||
|
|||||||
191
pages/index.js
191
pages/index.js
@@ -1,179 +1,22 @@
|
|||||||
import React, { Component } from 'react'
|
import React from 'react'
|
||||||
import { connect } from 'react-redux'
|
import config from '../src/util/pubConfig'
|
||||||
import Router from 'next/router'
|
import loadDocs from '../src/client/util/loadDocs'
|
||||||
import Paginate from 'react-paginate'
|
import ListDocs from '../src/client/comps/listDocs'
|
||||||
import { format } from 'url'
|
import RequireUser from '../src/client/comps/requireUser'
|
||||||
import Page from '../src/components/Page'
|
|
||||||
import PaddedRow from '../src/components/PaddedRow'
|
|
||||||
import Spinner from '../src/components/Spinner'
|
|
||||||
import DocItem from '../src/components/DocItem'
|
|
||||||
import { $limit, getDocs, buildQ } from '../src/util/getDocs'
|
|
||||||
import getJwt from '../src/util/getJwt'
|
|
||||||
import getUrl from '../src/util/getUrl'
|
|
||||||
import mapUser from '../src/util/mapUser'
|
|
||||||
|
|
||||||
class Index extends Component {
|
function Home(props) {
|
||||||
state = {
|
|
||||||
$sort: 'updated:-1',
|
|
||||||
$search: '',
|
|
||||||
page: 1,
|
|
||||||
pending: false,
|
|
||||||
error: null,
|
|
||||||
total: 0,
|
|
||||||
docs: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getInitialProps({ req, query }) {
|
|
||||||
let page = 1,
|
|
||||||
$search = ''
|
|
||||||
if (query) {
|
|
||||||
page = query.page || page
|
|
||||||
$search = query.search || $search
|
|
||||||
}
|
|
||||||
const jwt = getJwt(req)
|
|
||||||
if (!jwt) return { total: 0, docs: [] }
|
|
||||||
const q = buildQ({ $search, $skip: page })
|
|
||||||
const data = await getDocs(q, req ? jwt : false)
|
|
||||||
return { ...data, page, $search }
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
|
||||||
let { docs, total, page, $search } = nextProps
|
|
||||||
if (
|
|
||||||
!prevState.didInit &&
|
|
||||||
(page !== prevState.page || $search !== prevState.$search)
|
|
||||||
) {
|
|
||||||
return { total, docs, page, $search, pending: false, didInit: true }
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
pushQuery = query =>
|
|
||||||
Router.push(
|
|
||||||
{ pathname: '/', query },
|
|
||||||
format({ pathname: getUrl('/'), query })
|
|
||||||
)
|
|
||||||
|
|
||||||
updDocs = (time, doSearch) => {
|
|
||||||
clearTimeout(this.docsTime)
|
|
||||||
this.docsTime = setTimeout(async () => {
|
|
||||||
let { $sort, $search, page } = this.state
|
|
||||||
if (doSearch) {
|
|
||||||
const query = { search: $search }
|
|
||||||
if (!$search) delete query.search
|
|
||||||
this.pushQuery(query)
|
|
||||||
}
|
|
||||||
this.setState({ error: null })
|
|
||||||
this.docsTime = setTimeout(() => {
|
|
||||||
this.setState({ pending: true })
|
|
||||||
}, 125)
|
|
||||||
|
|
||||||
const q = buildQ({ $search, $sort, $skip: page })
|
|
||||||
const data = await getDocs(q)
|
|
||||||
clearTimeout(this.docsTime)
|
|
||||||
this.setState({ ...data, pending: false })
|
|
||||||
}, time || 275)
|
|
||||||
}
|
|
||||||
|
|
||||||
updQuery = e => {
|
|
||||||
this.setState({ [e.target.id]: e.target.value })
|
|
||||||
this.updDocs(0, e.target.id === '$search')
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePage = ({ selected }) => {
|
|
||||||
const { $search } = this.state
|
|
||||||
const page = selected + 1
|
|
||||||
const query = {}
|
|
||||||
this.setState({ page })
|
|
||||||
if (page > 1) query.page = page
|
|
||||||
if ($search) query.search = $search
|
|
||||||
this.pushQuery(query)
|
|
||||||
this.updDocs(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.updDocs(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { user, docs } = this.props
|
|
||||||
if (prevProps.user.email === user.email) return
|
|
||||||
if (user.email && docs.length === 0) this.updDocs(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { $sort, $search, pending, error, docs, total, page } = this.state
|
|
||||||
const pages = Math.ceil(total / $limit)
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<RequireUser>
|
||||||
<PaddedRow>
|
<ListDocs {...props} />
|
||||||
<input
|
</RequireUser>
|
||||||
type="text"
|
|
||||||
placeholder="Search knowledge base..."
|
|
||||||
maxLength={128}
|
|
||||||
value={$search}
|
|
||||||
className="search"
|
|
||||||
id="$search"
|
|
||||||
onChange={this.updQuery}
|
|
||||||
/>
|
|
||||||
</PaddedRow>
|
|
||||||
<PaddedRow>
|
|
||||||
<div className="inline" style={{ width: '100%' }}>
|
|
||||||
<h4 className="noMargin">Docs</h4>
|
|
||||||
<div className="float-right inline">
|
|
||||||
<label htmlFor="sort">Sort: </label>
|
|
||||||
<select
|
|
||||||
id="$sort"
|
|
||||||
value={$sort}
|
|
||||||
onChange={this.updQuery}
|
|
||||||
style={{ width: 150 }}
|
|
||||||
>
|
|
||||||
<option value="updated:-1">{'Updated (new -> old)'}</option>
|
|
||||||
<option value="updated:1">{'Updated (old -> new)'}</option>
|
|
||||||
<option value="created:-1">{'Created (new -> old)'}</option>
|
|
||||||
<option value="created:1">{'Created (old -> new)'}</option>
|
|
||||||
<option value="dirName:1">{'Name (A -> Z)'}</option>
|
|
||||||
<option value="dirName:-1">{'Name (Z -> A)'}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PaddedRow>
|
|
||||||
<PaddedRow>
|
|
||||||
{docs.length > 0 || error || pending ? null : <p>No docs found...</p>}
|
|
||||||
{!error ? null : <p>{error}</p>}
|
|
||||||
{!pending || error ? null : (
|
|
||||||
<Spinner style={{ margin: '25px auto 0' }} />
|
|
||||||
)}
|
|
||||||
{docs.length < 1 || pending || error ? null : (
|
|
||||||
<div>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
Doc <span className="float-right">Modified</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{docs.map(doc => (
|
|
||||||
<DocItem {...doc} key={doc.id} />
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{pages < 2 ? null : (
|
|
||||||
<Paginate
|
|
||||||
pageCount={pages}
|
|
||||||
containerClassName="paginate"
|
|
||||||
activeClassName="active"
|
|
||||||
onPageChange={this.handlePage}
|
|
||||||
forcePage={page - 1}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</PaddedRow>
|
|
||||||
</Page>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Home.getInitialProps = async ctx => {
|
||||||
|
if (config.ssr) {
|
||||||
|
await loadDocs(ctx.query || {})
|
||||||
}
|
}
|
||||||
export default connect(mapUser)(Index)
|
return { query: ctx.query }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
||||||
|
|||||||
65
pages/k.js
65
pages/k.js
@@ -1,65 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import Router from 'next/router'
|
|
||||||
import fetch from 'isomorphic-unfetch'
|
|
||||||
import Page from '../src/components/Page'
|
|
||||||
import Markdown from '../src/components/Markdown'
|
|
||||||
import AddDoc from '../src/components/AddDoc'
|
|
||||||
import getUrl from '../src/util/getUrl'
|
|
||||||
import getJwt from '../src/util/getJwt'
|
|
||||||
|
|
||||||
class k extends Component {
|
|
||||||
delete = async () => {
|
|
||||||
const sure = window.confirm(
|
|
||||||
'Are you sure you want to delete this doc? This can not be undone.'
|
|
||||||
)
|
|
||||||
if (!sure) return
|
|
||||||
const del = await fetch(getUrl('docs/' + this.props.id), {
|
|
||||||
headers: { Authorization: getJwt() },
|
|
||||||
method: 'DELETE',
|
|
||||||
}).catch(({ message }) => ({ ok: false, message }))
|
|
||||||
if (del.ok) Router.push('/', getUrl('/'))
|
|
||||||
else {
|
|
||||||
if (!del.message) {
|
|
||||||
const data = await del.json()
|
|
||||||
del.message = data.message
|
|
||||||
}
|
|
||||||
window.alert(`Could not delete doc, ${del.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { found, id, doc } = this.props
|
|
||||||
if (!found)
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<h3>Doc not found...</h3>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<h5 style={{ marginBottom: '1rem' }}>
|
|
||||||
{doc.dir}
|
|
||||||
{doc.dir.length > 0 ? '/' : ''}
|
|
||||||
{doc.name}
|
|
||||||
{' - '}
|
|
||||||
<Link
|
|
||||||
as={getUrl('edit/' + id)}
|
|
||||||
href={{ pathname: '/edit', query: { id } }}
|
|
||||||
>
|
|
||||||
<a id="edit">edit</a>
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
className="float-right"
|
|
||||||
onClick={this.delete}
|
|
||||||
style={{ margin: '5px 0 0' }}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</h5>
|
|
||||||
<Markdown source={doc.md} className="Markdown" />
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default AddDoc(k)
|
|
||||||
17
pages/new.js
17
pages/new.js
@@ -1,2 +1,15 @@
|
|||||||
import MngDoc from '../src/components/MngDoc'
|
import React from 'react'
|
||||||
export default MngDoc
|
import EditDoc from '../src/client/comps/editDoc'
|
||||||
|
import RequireUser from '../src/client/comps/requireUser'
|
||||||
|
|
||||||
|
function New({ query }) {
|
||||||
|
return (
|
||||||
|
<RequireUser>
|
||||||
|
<EditDoc {...{ query }} />
|
||||||
|
</RequireUser>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
New.getInitialProps = async ({ query }) => ({ query })
|
||||||
|
|
||||||
|
export default New
|
||||||
|
|||||||
23
pages/offline.js
Normal file
23
pages/offline.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import Router from 'next/router'
|
||||||
|
|
||||||
|
import addBase from '../src/util/addBase'
|
||||||
|
|
||||||
|
export default function Offline() {
|
||||||
|
// force next to render correct route on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const { pathname, search } = window.location
|
||||||
|
let origRoute = pathname + search
|
||||||
|
let curRoute = origRoute.split(addBase('/'))
|
||||||
|
curRoute.splice(0, 1)
|
||||||
|
curRoute = '/' + curRoute.join(addBase('/'))
|
||||||
|
|
||||||
|
if (curRoute === '/offline' && navigator.onLine) {
|
||||||
|
curRoute = '/'
|
||||||
|
origRoute = '/'
|
||||||
|
}
|
||||||
|
Router.push(curRoute, origRoute)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,114 +1,128 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import fetch from 'isomorphic-unfetch'
|
import addBase from '../src/util/addBase'
|
||||||
import Page from '../src/components/Page'
|
import getHeaders from '../src/client/util/getHeaders'
|
||||||
import PaddedRow from '../src/components/PaddedRow'
|
import RequireUser from '../src/client/comps/requireUser'
|
||||||
import Spinner from '../src/components/Spinner'
|
|
||||||
import updStateFromId from '../src/util/updStateFromId'
|
|
||||||
import mapUser from '../src/util/mapUser'
|
|
||||||
import getUrl from '../src/util/getUrl'
|
|
||||||
import getJwt from '../src/util/getJwt'
|
|
||||||
|
|
||||||
class Settings extends Component {
|
function Settings({ user }) {
|
||||||
state = {
|
const [data, setData] = useState({
|
||||||
pending: false,
|
current: '',
|
||||||
passErr: null,
|
confirm: '',
|
||||||
curPass: '',
|
new: '',
|
||||||
newPass: '',
|
})
|
||||||
confPass: '',
|
const [pending, setPending] = useState(false)
|
||||||
}
|
const [error, setError] = useState(null)
|
||||||
updVal = updStateFromId.bind(this)
|
|
||||||
submit = async e => {
|
const handleSubmit = () => {
|
||||||
e.preventDefault()
|
if (pending) return
|
||||||
const { pending, curPass, newPass, confPass } = this.state
|
let err
|
||||||
const { email, _id } = this.props.user
|
Object.keys(data).forEach(k => (data[k] = data[k].trim()))
|
||||||
if (pending) return
|
if (!data.current) err = 'current pass is required'
|
||||||
const doErr = passErr => this.setState({ pending: false, passErr })
|
else if (!data.new) err = 'new pass is required'
|
||||||
const vals = {
|
else if (!data.confirm) err = 'confirm pass is required'
|
||||||
'Current password': curPass,
|
else if (data.new !== data.confirm) err = 'new password must match confirm'
|
||||||
'New password': newPass,
|
|
||||||
'Confirm new password': confPass,
|
if (err) return setError(err)
|
||||||
}
|
setError(null)
|
||||||
const keys = Object.keys(vals)
|
setPending(true)
|
||||||
for (let i = 0; i < keys.length; i++) {
|
|
||||||
let key = keys[i],
|
fetch(addBase('/user'), {
|
||||||
val = vals[key]
|
method: 'PATCH',
|
||||||
if (val.length === 0) return doErr(`${key} is required`)
|
headers: {
|
||||||
}
|
...getHeaders(),
|
||||||
if (newPass !== confPass) return doErr("New passwords don't match")
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
this.setState({ passErr: null, pending: true })
|
body: JSON.stringify({
|
||||||
const updRes = await fetch(getUrl('users/' + _id), {
|
username: user.username,
|
||||||
method: 'PATCH',
|
password: data.current,
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: getJwt() },
|
newPassword: data.new,
|
||||||
body: JSON.stringify({ email, password: curPass, newPassword: newPass }),
|
}),
|
||||||
}).catch(doErr)
|
})
|
||||||
if (updRes.ok) {
|
.then(async res => {
|
||||||
this.setState({
|
const { status, ...data } = await res.json()
|
||||||
curPass: '',
|
if (status === 'ok') {
|
||||||
newPass: '',
|
setPending(false)
|
||||||
confPass: '',
|
setError('Password updated')
|
||||||
passErr: 'Password updated successfully',
|
return setData({
|
||||||
pending: false,
|
current: '',
|
||||||
|
confirm: '',
|
||||||
|
new: '',
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
let message = 'failed to update password'
|
|
||||||
try {
|
|
||||||
const data = await updRes.json()
|
|
||||||
message = data.message || message
|
|
||||||
} catch (err) {
|
|
||||||
doErr(err.message)
|
|
||||||
}
|
}
|
||||||
doErr(message)
|
throw new Error(data.message)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
setPending(false)
|
||||||
|
setError(err.message || 'An error occurred updating password')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
const handleChange = e => {
|
||||||
|
data[e.target.id] = e.target.value
|
||||||
|
setData(data)
|
||||||
}
|
}
|
||||||
render() {
|
|
||||||
const { pending, passErr, curPass, newPass, confPass } = this.state
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<RequireUser>
|
||||||
<PaddedRow amount={25}>
|
<div className="container padded fill">
|
||||||
<h3>Account settings</h3>
|
<h3>Account settings</h3>
|
||||||
<hr />
|
<hr />
|
||||||
<form noValidate style={{ padding: '0 0 45px' }}>
|
|
||||||
<h4>Change password</h4>
|
<h4>Change password</h4>
|
||||||
<fieldset>
|
|
||||||
<label htmlFor="curPass">Current Password</label>
|
<label htmlFor="current">Current Password</label>
|
||||||
<input
|
<input
|
||||||
|
id="current"
|
||||||
type="password"
|
type="password"
|
||||||
id="curPass"
|
placeholder="Current super secret password"
|
||||||
onChange={this.updVal}
|
value={data.current}
|
||||||
placeholder="Current super secret password..."
|
onChange={handleChange}
|
||||||
value={curPass}
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="newPass">New Password</label>
|
|
||||||
|
<label htmlFor="new">New Password</label>
|
||||||
<input
|
<input
|
||||||
|
id="new"
|
||||||
type="password"
|
type="password"
|
||||||
id="newPass"
|
placeholder="New super secret password"
|
||||||
onChange={this.updVal}
|
value={data.new}
|
||||||
placeholder="New super secret password..."
|
onChange={handleChange}
|
||||||
value={newPass}
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="confPass">Confirm New Password</label>
|
|
||||||
|
<label htmlFor="confirm">Confirm New Password</label>
|
||||||
<input
|
<input
|
||||||
|
id="confirm"
|
||||||
type="password"
|
type="password"
|
||||||
id="confPass"
|
value={data.confirm}
|
||||||
onChange={this.updVal}
|
onChange={handleChange}
|
||||||
placeholder="Confirm new super secret password..."
|
placeholder="Confirm its not too secret you forgot"
|
||||||
value={confPass}
|
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
|
||||||
<button
|
<div>
|
||||||
onClick={this.submit}
|
{error && <p className="float-left">{error}</p>}
|
||||||
className={'float-right' + (pending ? ' disabled' : '')}
|
<button className="float-right" onClick={handleSubmit}>
|
||||||
>
|
Submit
|
||||||
{pending ? <Spinner /> : 'Submit'}
|
|
||||||
</button>
|
</button>
|
||||||
{!passErr ? null : <p>{passErr}</p>}
|
</div>
|
||||||
</form>
|
|
||||||
</PaddedRow>
|
<style jsx>{`
|
||||||
</Page>
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px;
|
||||||
|
max-width: 550px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
</RequireUser>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
export default connect(mapUser)(Settings)
|
export default connect(({ user }) => ({ user }))(Settings)
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
// Application hooks that run for every service
|
|
||||||
const logger = require('./hooks/logger')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
before: {
|
|
||||||
all: [logger()],
|
|
||||||
find: [],
|
|
||||||
get: [],
|
|
||||||
create: [],
|
|
||||||
update: [],
|
|
||||||
patch: [],
|
|
||||||
remove: [],
|
|
||||||
},
|
|
||||||
|
|
||||||
after: {
|
|
||||||
all: [logger()],
|
|
||||||
find: [],
|
|
||||||
get: [],
|
|
||||||
create: [],
|
|
||||||
update: [],
|
|
||||||
patch: [],
|
|
||||||
remove: [],
|
|
||||||
},
|
|
||||||
|
|
||||||
error: {
|
|
||||||
all: [logger()],
|
|
||||||
find: [],
|
|
||||||
get: [],
|
|
||||||
create: [],
|
|
||||||
update: [],
|
|
||||||
patch: [],
|
|
||||||
remove: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
133
src/app.js
133
src/app.js
@@ -1,133 +0,0 @@
|
|||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const compress = require('compression')
|
|
||||||
const cors = require('cors')
|
|
||||||
const helmet = require('helmet')
|
|
||||||
const cookieParser = require('cookie-parser')()
|
|
||||||
const RateLimit = require('express-rate-limit')
|
|
||||||
const trustIPs = require('./trustIPs')
|
|
||||||
const logger = require('winston')
|
|
||||||
|
|
||||||
const feathers = require('@feathersjs/feathers')
|
|
||||||
const express = require('@feathersjs/express')
|
|
||||||
const configuration = require('@feathersjs/configuration')
|
|
||||||
const hostConfig = require('../config/host.json')
|
|
||||||
|
|
||||||
const middleware = require('./middleware')
|
|
||||||
const services = require('./services')
|
|
||||||
const appHooks = require('./app.hooks')
|
|
||||||
const channels = require('./channels')
|
|
||||||
const authentication = require('./authentication')
|
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV !== 'production'
|
|
||||||
const pathPrefix = require('./util/pathPrefix')
|
|
||||||
const stripBase = require('./util/stripPrefix')
|
|
||||||
const getUrl = require('./util/getUrl')
|
|
||||||
const { parse } = require('url')
|
|
||||||
const Next = require('next')({ dev, quiet: true })
|
|
||||||
const nextHandler = Next.getRequestHandler()
|
|
||||||
|
|
||||||
const app = express(feathers())
|
|
||||||
global.app = app
|
|
||||||
|
|
||||||
app.run = async port => {
|
|
||||||
const server = app.listen(port)
|
|
||||||
await Next.prepare()
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
server.on('upgrade', (req, socket) => {
|
|
||||||
req.url = stripBase(req.url)
|
|
||||||
nextHandler(req, socket, parse(req.url, true))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load app configuration
|
|
||||||
app.configure(configuration())
|
|
||||||
|
|
||||||
// load host and setup settings
|
|
||||||
Object.keys(hostConfig).forEach(key => app.set(key, hostConfig[key]))
|
|
||||||
app.set('kbConf', {
|
|
||||||
pathPrefix,
|
|
||||||
})
|
|
||||||
app.set('didSetup', false)
|
|
||||||
try {
|
|
||||||
fs.statSync(path.join(__dirname, '..', 'db', '.didSetup'))
|
|
||||||
app.set('didSetup', true)
|
|
||||||
} catch (err) {
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
req.doSetup = !app.get('didSetup')
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const authLimit = new RateLimit({
|
|
||||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
||||||
max: 5, // 5 attempts then block
|
|
||||||
delayAfter: 3, // slow down after 3 fails
|
|
||||||
delayMs: 2 * 1000,
|
|
||||||
})
|
|
||||||
app.authLimit = authLimit
|
|
||||||
app.use(getUrl('auth'), authLimit)
|
|
||||||
app.patch(getUrl('users/*'), authLimit)
|
|
||||||
|
|
||||||
// Enable CORS, security, compression, favicon and body parsing
|
|
||||||
trustIPs(app)
|
|
||||||
app.use(cors())
|
|
||||||
app.use(
|
|
||||||
helmet({
|
|
||||||
hidePoweredBy: { setTo: 'hamsters' },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
app.use(getUrl('/'), express.static(path.join(__dirname, '../public')))
|
|
||||||
|
|
||||||
if (!dev) app.use(compress())
|
|
||||||
app.use(express.json()) // use { limit } option to increase max post size
|
|
||||||
app.use(express.urlencoded({ extended: true }))
|
|
||||||
app.configure(express.rest()) // Set up Plugins and providers
|
|
||||||
app.configure(middleware) // middleware/index.js
|
|
||||||
app.configure(authentication) // Set up authentication
|
|
||||||
app.configure(services) // Set up our services (see `services/index.js`)
|
|
||||||
app.configure(channels) // Set up event channels (see channels.js)
|
|
||||||
|
|
||||||
Next.setAssetPrefix(pathPrefix)
|
|
||||||
|
|
||||||
const checkJWT = async (req, res, next) => {
|
|
||||||
const result = await req.app.authenticate('jwt', {})(req)
|
|
||||||
if (result.success) {
|
|
||||||
req.jwt = req.cookies.jwt
|
|
||||||
delete result.data.user.password
|
|
||||||
req.user = result.data.user
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
;['/', '/logout', '/new', '/settings'].forEach(route => {
|
|
||||||
app.get(getUrl(route), cookieParser, checkJWT, (req, res) => {
|
|
||||||
const { query } = parse(req.url, true)
|
|
||||||
Next.render(req, res, route, query)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
;['/k', '/edit'].forEach(route => {
|
|
||||||
app.get(getUrl(route + '/:id'), cookieParser, checkJWT, (req, res) => {
|
|
||||||
Next.render(req, res, route, { id: req.params.id })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const notFound = express.notFound()
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
let accept = req.get('accept')
|
|
||||||
if (accept && accept.toLowerCase() === 'application/json')
|
|
||||||
return notFound(req, res, next)
|
|
||||||
if (req.url.substr(0, pathPrefix.length) !== pathPrefix)
|
|
||||||
return Next.render404(req, res)
|
|
||||||
|
|
||||||
req.url = stripBase(req.url)
|
|
||||||
nextHandler(req, res, parse(req.url, true))
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(express.errorHandler({ logger }))
|
|
||||||
app.hooks(appHooks)
|
|
||||||
|
|
||||||
module.exports = app
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
const authentication = require('@feathersjs/authentication')
|
|
||||||
const jwt = require('@feathersjs/authentication-jwt')
|
|
||||||
const local = require('@feathersjs/authentication-local')
|
|
||||||
const getUrl = require('./util/getUrl')
|
|
||||||
|
|
||||||
module.exports = function(app) {
|
|
||||||
const config = app.get('authentication')
|
|
||||||
config.path = getUrl(config.path)
|
|
||||||
config.service = getUrl('users')
|
|
||||||
|
|
||||||
// Set up authentication with the secret
|
|
||||||
app.configure(
|
|
||||||
authentication(
|
|
||||||
Object.assign({}, config, {
|
|
||||||
cookie: {
|
|
||||||
enabled: true,
|
|
||||||
httpOnly: false,
|
|
||||||
secure: false,
|
|
||||||
name: 'jwt',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
app.configure(jwt())
|
|
||||||
app.configure(local())
|
|
||||||
|
|
||||||
// The `authentication` service is used to create a JWT.
|
|
||||||
// The before `create` hook registers strategies that can be used
|
|
||||||
// to create a new valid JWT (e.g. local or oauth2)
|
|
||||||
app.service(config.path).hooks({
|
|
||||||
before: {
|
|
||||||
create: [
|
|
||||||
authentication.hooks.authenticate(config.strategies),
|
|
||||||
ctx => {
|
|
||||||
ctx.app.authLimit.resetKey(ctx.params.ip)
|
|
||||||
return ctx
|
|
||||||
},
|
|
||||||
],
|
|
||||||
remove: [authentication.hooks.authenticate('jwt')],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
module.exports = function(app) {
|
|
||||||
if (typeof app.channel !== 'function') {
|
|
||||||
// If no real-time functionality has been configured just return
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('connection', connection => {
|
|
||||||
// On a new real-time connection, add it to the anonymous channel
|
|
||||||
app.channel('anonymous').join(connection)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('login', (authResult, { connection }) => {
|
|
||||||
// connection can be undefined if there is no
|
|
||||||
// real-time connection, e.g. when logging in via REST
|
|
||||||
if (connection) {
|
|
||||||
// Obtain the logged in user from the connection
|
|
||||||
// const user = connection.user;
|
|
||||||
|
|
||||||
// The connection is no longer anonymous, remove it
|
|
||||||
app.channel('anonymous').leave(connection)
|
|
||||||
|
|
||||||
// Add it to the authenticated user channel
|
|
||||||
app.channel('authenticated').join(connection)
|
|
||||||
|
|
||||||
// Channels can be named anything and joined on any condition
|
|
||||||
|
|
||||||
// E.g. to send real-time events only to admins use
|
|
||||||
// if(user.isAdmin) { app.channel('admins').join(connection); }
|
|
||||||
|
|
||||||
// If the user has joined e.g. chat rooms
|
|
||||||
// if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(channel));
|
|
||||||
|
|
||||||
// Easily organize users by email and userid for things like messaging
|
|
||||||
// app.channel(`emails/${user.email}`).join(channel);
|
|
||||||
// app.channel(`userIds/$(user.id}`).join(channel);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
app.publish((data, hook) => {
|
|
||||||
// Here you can add event publishers to channels set up in `channels.js`
|
|
||||||
// To publish only for a specific event use `app.publish(eventname, () => {})`
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.log(
|
|
||||||
'Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'
|
|
||||||
)
|
|
||||||
|
|
||||||
// e.g. to publish all service events to all authenticated users use
|
|
||||||
return app.channel('authenticated')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Here you can also add service specific event publishers
|
|
||||||
// e..g the publish the `users` service `created` event to the `admins` channel
|
|
||||||
// app.service('users').publish('created', () => app.channel('admins'));
|
|
||||||
|
|
||||||
// With the userid and email organization from above you can easily select involved users
|
|
||||||
// app.service('messages').publish(() => {
|
|
||||||
// return [
|
|
||||||
// app.channel(`userIds/${data.createdBy}`),
|
|
||||||
// app.channel(`emails/${data.recipientEmail}`)
|
|
||||||
// ];
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
17
src/client/actionTypes.js
Normal file
17
src/client/actionTypes.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import mirror from 'keymirror'
|
||||||
|
|
||||||
|
export default mirror({
|
||||||
|
// user actions
|
||||||
|
SET_USER: null,
|
||||||
|
USER_LOGOUT: null,
|
||||||
|
|
||||||
|
// docs actions
|
||||||
|
DOC_DELETED: null,
|
||||||
|
DOCS_ERROR: null,
|
||||||
|
DOCS_LOADED: null,
|
||||||
|
DOCS_PENDING: null,
|
||||||
|
|
||||||
|
// cache actions
|
||||||
|
LOAD_CACHE: null,
|
||||||
|
CACHE_DOCS: null,
|
||||||
|
})
|
||||||
59
src/client/comps/codemirror.js
Normal file
59
src/client/comps/codemirror.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import config from '../../util/pubConfig'
|
||||||
|
import MonokaiStyles from '../styles/codemirror/monokai'
|
||||||
|
import CodeMirrorStyles from '../styles/codemirror/codemirror'
|
||||||
|
|
||||||
|
let cm
|
||||||
|
let editor
|
||||||
|
let textareaRef
|
||||||
|
|
||||||
|
if (!config.ssr) {
|
||||||
|
require('codemirror/mode/markdown/markdown')
|
||||||
|
cm = require('codemirror')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeMirror({
|
||||||
|
value,
|
||||||
|
style,
|
||||||
|
className,
|
||||||
|
options = {},
|
||||||
|
onChange = () => {},
|
||||||
|
onSubmit = () => {},
|
||||||
|
}) {
|
||||||
|
const handleChange = (cm, e) => onChange(cm.getValue())
|
||||||
|
const handleSubmit = (cm, e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
onSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cm) {
|
||||||
|
if (!editor || editor.getTextArea() !== textareaRef) {
|
||||||
|
editor = cm.fromTextArea(textareaRef, options)
|
||||||
|
}
|
||||||
|
editor.on('change', handleChange)
|
||||||
|
editor.on('keydown', handleSubmit)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (editor) {
|
||||||
|
editor.off('change', handleChange)
|
||||||
|
editor.off('keydown', handleSubmit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...{ className, style }}>
|
||||||
|
<textarea
|
||||||
|
{...{
|
||||||
|
defaultValue: value,
|
||||||
|
ref: el => (textareaRef = el),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MonokaiStyles />
|
||||||
|
<CodeMirrorStyles />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
104
src/client/comps/editDoc.js
Normal file
104
src/client/comps/editDoc.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { updateDoc } from '../util/docHelpers'
|
||||||
|
import isOkDirPart from '../../util/isOkDirPart'
|
||||||
|
|
||||||
|
const Markdown = dynamic(() => import('react-markdown'))
|
||||||
|
const CodeMirror = dynamic(() => import('./codemirror'))
|
||||||
|
const dirError =
|
||||||
|
'contains an invalid character, must only have a-z, 0-9, -, _, and not start or end with a period or space'
|
||||||
|
|
||||||
|
function EditDoc({ cache, query }) {
|
||||||
|
const doc = cache[query.id] || {}
|
||||||
|
const [md, setMd] = useState(
|
||||||
|
doc.md || '### New document\n\nHeres some starting text'
|
||||||
|
)
|
||||||
|
const [dir, setDir] = useState(doc.dir || '')
|
||||||
|
const [name, setName] = useState(doc.name || '')
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const [pending, setPending] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (pending) return
|
||||||
|
let err
|
||||||
|
if (!name.trim()) {
|
||||||
|
err = 'Name is required'
|
||||||
|
} else if (!isOkDirPart(name)) {
|
||||||
|
err = `Name ${dirError}`
|
||||||
|
} else if (dir && dir.split('/').some(dirPart => !isOkDirPart(dirPart))) {
|
||||||
|
err = `Directory ${dirError}`
|
||||||
|
} else if (!md.trim()) {
|
||||||
|
err = 'Contents of markdown can not be empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) return setError(err)
|
||||||
|
setError(null)
|
||||||
|
setPending(true)
|
||||||
|
updateDoc(query.id, md, name.trim(), dir).catch(err => {
|
||||||
|
setError(err.message)
|
||||||
|
setPending(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container padded editDoc">
|
||||||
|
<div className="row">
|
||||||
|
<div className="column column-50">
|
||||||
|
<Markdown source={md} className="Markdown" />
|
||||||
|
</div>
|
||||||
|
<div className="column column-50">
|
||||||
|
<div className="row">
|
||||||
|
<div className="column column-60">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
maxLength={255}
|
||||||
|
placeholder="Document name"
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column column">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={dir}
|
||||||
|
placeholder="Directory (optional)"
|
||||||
|
onChange={e => setDir(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<CodeMirror
|
||||||
|
value={md}
|
||||||
|
onChange={setMd}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
className="column wrapCodeMirror"
|
||||||
|
options={{
|
||||||
|
theme: 'monokai',
|
||||||
|
mode: 'markdown',
|
||||||
|
lineWrapping: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="row" style={{ paddingTop: 15 }}>
|
||||||
|
<div className="column">
|
||||||
|
{error && <p className="float-left">{error}</p>}
|
||||||
|
<button className="float-right" onClick={handleSubmit}>
|
||||||
|
{pending ? 'Pending' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style jsx global>{`
|
||||||
|
.wrapCodeMirror textarea {
|
||||||
|
margin-bottom: 0;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(({ cache }) => ({ cache }))(EditDoc)
|
||||||
9
src/client/comps/extLink.js
Normal file
9
src/client/comps/extLink.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function extLink({ children, ...props }) {
|
||||||
|
return (
|
||||||
|
<a rel="noopener noreferrer" target="_blank" {...props}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
172
src/client/comps/listDocs.js
Normal file
172
src/client/comps/listDocs.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import config from '../../util/pubConfig'
|
||||||
|
import addBase from '../../util/addBase'
|
||||||
|
import loadDocs from '../util/loadDocs'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import Paginate from 'react-paginate'
|
||||||
|
import Router from 'next/router'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
let searchTimeout
|
||||||
|
let abortController
|
||||||
|
|
||||||
|
const { date, defDocsLimit, searchDelay, ssr } = config
|
||||||
|
const abort = () => {
|
||||||
|
abortController && abortController.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListDocs({ docs, query }) {
|
||||||
|
const [offline, setOffline] = useState(ssr ? false : !navigator.onLine)
|
||||||
|
const curSort = query.sort || 'updated:-1'
|
||||||
|
const curPage = parseInt(query.page, 10) || 1
|
||||||
|
const pageCount = Math.ceil(docs.total / defDocsLimit)
|
||||||
|
const handleOffline = () => setOffline(true)
|
||||||
|
const handleOnline = () => setOffline(false)
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
abortController = loadDocs(query)
|
||||||
|
window.addEventListener('online', handleOnline)
|
||||||
|
window.addEventListener('offline', handleOffline)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
abort()
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
window.removeEventListener('online', handleOnline)
|
||||||
|
window.removeEventListener('offline', handleOffline)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[query]
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateUrl = query => {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
Router.push({ pathname: '/', query }, { pathname: addBase('/'), query })
|
||||||
|
}, searchDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleField = e => {
|
||||||
|
const key = e.target.getAttribute('name')
|
||||||
|
updateUrl({
|
||||||
|
...query,
|
||||||
|
page: 1,
|
||||||
|
[key]: e.target.value.trim(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePage = ({ selected }) => {
|
||||||
|
if (curPage - 1 === selected) return
|
||||||
|
updateUrl({
|
||||||
|
...query,
|
||||||
|
page: selected + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container padded">
|
||||||
|
<form action={addBase('/')} method="GET">
|
||||||
|
<div className="row">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="search"
|
||||||
|
maxLength={300}
|
||||||
|
className="search"
|
||||||
|
onChange={handleField}
|
||||||
|
defaultValue={query.search || ''}
|
||||||
|
placeholder="Search knowledge base..."
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
name="sort"
|
||||||
|
value={curSort}
|
||||||
|
onChange={handleField}
|
||||||
|
className="column column-25"
|
||||||
|
>
|
||||||
|
<option value="updated:-1">Updated (new to old)</option>
|
||||||
|
<option value="updated:1">Updated (old to new)</option>
|
||||||
|
<option value="created:-1">Created (new to old)</option>
|
||||||
|
<option value="created:1">Created (old to new)</option>
|
||||||
|
<option value="id:1">Path (A to Z)</option>
|
||||||
|
<option value="id:-1">Path (Z to A)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<noscript>
|
||||||
|
<button type="submit" className="float-right">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</noscript>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{docs.error && <p>{docs.error} </p>}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Doc{offline && ` (offline mode)`}</th>
|
||||||
|
<th>Modified</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{docs.results.map(doc => {
|
||||||
|
const docUrl = { pathname: '/doc', query: { id: doc.id } }
|
||||||
|
return (
|
||||||
|
<tr key={doc.id}>
|
||||||
|
<td>
|
||||||
|
<Link
|
||||||
|
href={docUrl}
|
||||||
|
as={{ ...docUrl, pathname: addBase('/doc') }}
|
||||||
|
>
|
||||||
|
<a>{doc.id}</a>
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{new Date(doc.updated).toLocaleDateString(
|
||||||
|
date.locale,
|
||||||
|
date.options
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{!docs.total && <p>No docs found...</p>}
|
||||||
|
|
||||||
|
{docs.total > defDocsLimit && (
|
||||||
|
<Paginate
|
||||||
|
previousLabel="Prev"
|
||||||
|
pageCount={pageCount}
|
||||||
|
marginPagesDisplayed={2}
|
||||||
|
activeClassName="active"
|
||||||
|
forcePage={curPage - 1}
|
||||||
|
onPageChange={handlePage}
|
||||||
|
containerClassName="paginate"
|
||||||
|
hrefBuilder={pg => addBase(`/?page=${pg}`)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
.container {
|
||||||
|
max-width: 750px;
|
||||||
|
margin: 15px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row input {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:nth-of-type(2n),
|
||||||
|
td:nth-of-type(2n) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(({ docs }) => ({ docs }))(ListDocs)
|
||||||
9
src/client/comps/requireUser.js
Normal file
9
src/client/comps/requireUser.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import Login from '../forms/login'
|
||||||
|
|
||||||
|
function RequireUser({ children, user }) {
|
||||||
|
return user.id ? children : <Login doSetup={user.doSetup} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(({ user }) => ({ user }))(RequireUser)
|
||||||
72
src/client/comps/shortcuts.js
Normal file
72
src/client/comps/shortcuts.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import Router from 'next/router'
|
||||||
|
import logout from '../util/logout'
|
||||||
|
import addBase from '../../util/addBase'
|
||||||
|
|
||||||
|
/* - keyboard shortcuts
|
||||||
|
g then h -> navigate home
|
||||||
|
g then n -> navigate to new doc
|
||||||
|
g then s -> navigate to settings
|
||||||
|
g then l -> logout
|
||||||
|
e (when on doc page) -> edit doc
|
||||||
|
/ (when on home page) -> focus search
|
||||||
|
ctrl/cmd + enter -> submit new doc (handled in CodeMirror component)
|
||||||
|
*/
|
||||||
|
const keyToUrl = {
|
||||||
|
72: '/',
|
||||||
|
78: '/new',
|
||||||
|
83: '/settings',
|
||||||
|
}
|
||||||
|
const keyToEl = {
|
||||||
|
69: { sel: '#edit', func: 'click' },
|
||||||
|
191: { sel: '.search', func: 'focus' },
|
||||||
|
}
|
||||||
|
const getKey = e => e.which || e.keyCode
|
||||||
|
|
||||||
|
let prevKey
|
||||||
|
|
||||||
|
const handleKeyDown = e => {
|
||||||
|
const tag = e.target.tagName
|
||||||
|
if (tag === 'INPUT' || tag === 'TEXTAREA') return
|
||||||
|
const key = getKey(e)
|
||||||
|
if (prevKey === 71) {
|
||||||
|
// prev key was g
|
||||||
|
switch (key) {
|
||||||
|
case 72:
|
||||||
|
case 78:
|
||||||
|
case 83: {
|
||||||
|
const url = keyToUrl[key]
|
||||||
|
Router.push(url, addBase(url))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 76: {
|
||||||
|
// logout
|
||||||
|
setTimeout(logout, 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (key) {
|
||||||
|
case 69:
|
||||||
|
case 191: {
|
||||||
|
const { sel, func } = keyToEl[key]
|
||||||
|
const el = document.querySelector(sel)
|
||||||
|
if (el) setTimeout(() => el[func](), 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
prevKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Shortcuts(props) {
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
112
src/client/forms/login.js
Normal file
112
src/client/forms/login.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import addBase from '../../util/addBase'
|
||||||
|
import actionTypes from '../actionTypes'
|
||||||
|
import { getStore } from '../store'
|
||||||
|
|
||||||
|
export default function Login({ doSetup }) {
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const [pending, setPending] = useState(false)
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const endpoint = addBase(doSetup ? '/register' : '/auth')
|
||||||
|
|
||||||
|
useEffect(() => setMounted(true), [])
|
||||||
|
|
||||||
|
const submit = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (pending || !username || !password) return
|
||||||
|
setPending(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.status === 429) {
|
||||||
|
throw new Error('Too many login attempts')
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
})
|
||||||
|
.then(({ accessToken, message }) => {
|
||||||
|
if (message) {
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
localStorage.setItem('jwt', accessToken)
|
||||||
|
getStore().dispatch({
|
||||||
|
type: actionTypes.SET_USER,
|
||||||
|
user: {
|
||||||
|
...JSON.parse(atob(accessToken.split('.')[1])).user,
|
||||||
|
jwt: accessToken,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
setError(err.message)
|
||||||
|
setPending(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fill">
|
||||||
|
<h4>{doSetup ? 'Setup account' : 'Please login to continue'}</h4>
|
||||||
|
|
||||||
|
<form action={endpoint} method="post">
|
||||||
|
<label>
|
||||||
|
Username:
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
value={username}
|
||||||
|
placeholder="Username"
|
||||||
|
onChange={e => setUsername(e.target.value.trim())}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Password:
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={password}
|
||||||
|
placeholder="Super secret password"
|
||||||
|
onChange={e => setPassword(e.target.value.trim())}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{!mounted && <input type="hidden" value="true" name="form" />}
|
||||||
|
{error && <p className="float-left">{error}</p>}
|
||||||
|
<button disabled={pending} onClick={submit}>
|
||||||
|
{pending ? 'Pending...' : 'SUBMIT'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
div {
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px;
|
||||||
|
max-width: 550px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
58
src/client/hocs/withRedux.js
Normal file
58
src/client/hocs/withRedux.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import { initializeStore } from '../store'
|
||||||
|
import checkLogin from '../util/checkLogin'
|
||||||
|
import config from '../../util/pubConfig'
|
||||||
|
|
||||||
|
const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__'
|
||||||
|
|
||||||
|
function getOrCreateStore(initialState) {
|
||||||
|
// Always make a new store if server, otherwise state is shared between requests
|
||||||
|
if (config.ssr) {
|
||||||
|
global.reduxStore = initializeStore(initialState)
|
||||||
|
return global.reduxStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create store if unavailable on the client and set it on the window object
|
||||||
|
if (!window[__NEXT_REDUX_STORE__]) {
|
||||||
|
window[__NEXT_REDUX_STORE__] = initializeStore(initialState)
|
||||||
|
}
|
||||||
|
return window[__NEXT_REDUX_STORE__]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App => {
|
||||||
|
return class AppWithRedux extends Component {
|
||||||
|
static async getInitialProps(appContext) {
|
||||||
|
// Get or Create the store with `undefined` as initialState
|
||||||
|
// This allows you to set a custom default initialState
|
||||||
|
const reduxStore = getOrCreateStore()
|
||||||
|
|
||||||
|
// Provide the store to getInitialProps of pages
|
||||||
|
appContext.ctx.reduxStore = reduxStore
|
||||||
|
|
||||||
|
await checkLogin(appContext.ctx.req)
|
||||||
|
|
||||||
|
let appProps = {}
|
||||||
|
if (typeof App.getInitialProps === 'function') {
|
||||||
|
appProps = await App.getInitialProps.call(App, appContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...appProps,
|
||||||
|
initialReduxState: reduxStore.getState(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.reduxStore = getOrCreateStore(props.initialReduxState)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
checkLogin()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <App {...this.props} reduxStore={this.reduxStore} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/client/layout/footer.js
Normal file
30
src/client/layout/footer.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import theme from '../theme'
|
||||||
|
import ExtLink from '../comps/extLink'
|
||||||
|
|
||||||
|
export default function Footer(props) {
|
||||||
|
return (
|
||||||
|
<footer>
|
||||||
|
<p>
|
||||||
|
<span>Powered by</span>
|
||||||
|
<ExtLink href="//github.com/ijjk/mykb">MYKB</ExtLink>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
background: ${theme.primaryAlt};
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
padding: 10px 10px 15px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
164
src/client/layout/header.js
Normal file
164
src/client/layout/header.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import theme from '../theme'
|
||||||
|
import logout from '../util/logout'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { withRouter } from 'next/router'
|
||||||
|
import addBase from '../../util/addBase'
|
||||||
|
|
||||||
|
const navLinks = [
|
||||||
|
{ link: '/', label: 'Home' },
|
||||||
|
{ link: '/new', label: 'New' },
|
||||||
|
{ link: '/settings', label: 'Settings' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function Header({ user, router }) {
|
||||||
|
const curPath = addBase(router.pathname)
|
||||||
|
const [menuOpen, setOpen] = useState(false)
|
||||||
|
const handleChange = () => setOpen(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
router.events.on('routeChangeComplete', handleChange)
|
||||||
|
return () => router.events.off('routeChangeComplete', handleChange)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
<h3>
|
||||||
|
<Link href="/" as={addBase('/')}>
|
||||||
|
<a>MYKB</a>
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{user.id && (
|
||||||
|
<>
|
||||||
|
<label htmlFor="menu">MENU</label>
|
||||||
|
<input
|
||||||
|
id="menu"
|
||||||
|
type="checkbox"
|
||||||
|
checked={menuOpen}
|
||||||
|
onChange={e => setOpen(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
{user.id && (
|
||||||
|
<ul>
|
||||||
|
{navLinks.map(({ link, label }) => (
|
||||||
|
<li key={label}>
|
||||||
|
<Link href={link} as={addBase(link)}>
|
||||||
|
<a className={addBase(link) === curPath ? 'active' : null}>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li>
|
||||||
|
<a href={addBase('/logout')} onClick={logout}>
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
header {
|
||||||
|
height: 55px;
|
||||||
|
display: flex;
|
||||||
|
padding: 0 10px;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background: ${theme.primaryAlt};
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: auto;
|
||||||
|
user-select: none;
|
||||||
|
color: ${theme.link};
|
||||||
|
}
|
||||||
|
input[type='checkbox'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
left: 0;
|
||||||
|
top: 55px;
|
||||||
|
height: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
position: fixed;
|
||||||
|
overflow: hidden;
|
||||||
|
background: ${theme.primaryAlt};
|
||||||
|
transition: height 200ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked ~ nav {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li a {
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
padding: 10px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all ease 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
li a.active,
|
||||||
|
li a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: ${theme.primary};
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 420px) {
|
||||||
|
label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav,
|
||||||
|
ul {
|
||||||
|
top: 0;
|
||||||
|
left: none;
|
||||||
|
height: 55px;
|
||||||
|
width: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li a {
|
||||||
|
height: 55px;
|
||||||
|
width: initial;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(connect(({ user }) => ({ user }))(Header))
|
||||||
51
src/client/store.js
Normal file
51
src/client/store.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { combineReducers, compose, createStore } from 'redux'
|
||||||
|
import config from '../util/pubConfig'
|
||||||
|
import actionTypes from './actionTypes'
|
||||||
|
|
||||||
|
// import stores
|
||||||
|
import user from './stores/userStore'
|
||||||
|
import docs from './stores/docsStore'
|
||||||
|
import cache from './stores/cacheStore'
|
||||||
|
|
||||||
|
export function initializeStore(initialState) {
|
||||||
|
let enhancer = undefined
|
||||||
|
|
||||||
|
if (!config.ssr) {
|
||||||
|
const persistState = require('redux-localstorage')
|
||||||
|
enhancer = compose(
|
||||||
|
persistState(['cache', 'user'], {
|
||||||
|
merge: (initial, persisted) => {
|
||||||
|
return {
|
||||||
|
...initial,
|
||||||
|
cache: {
|
||||||
|
...persisted.cache,
|
||||||
|
...initial.cache,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
...initial.user,
|
||||||
|
...persisted.user,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const store = combineReducers({
|
||||||
|
user,
|
||||||
|
docs,
|
||||||
|
cache,
|
||||||
|
})
|
||||||
|
const rootStore = (state, action) => {
|
||||||
|
if (action.type === actionTypes.USER_LOGOUT) {
|
||||||
|
state = undefined
|
||||||
|
} else if (action.type === actionTypes.LOAD_CACHE) {
|
||||||
|
state = action.state
|
||||||
|
}
|
||||||
|
return store(state, action)
|
||||||
|
}
|
||||||
|
return createStore(rootStore, initialState, enhancer)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStore() {
|
||||||
|
return config.ssr ? global.reduxStore : window['__NEXT_REDUX_STORE__']
|
||||||
|
}
|
||||||
27
src/client/stores/cacheStore.js
Normal file
27
src/client/stores/cacheStore.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import actionTypes from '../actionTypes'
|
||||||
|
|
||||||
|
const initialState = {}
|
||||||
|
|
||||||
|
export default function cache(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case actionTypes.DOCS_LOADED:
|
||||||
|
case actionTypes.CACHE_DOCS: {
|
||||||
|
if (!action.data || !action.data.results) return state
|
||||||
|
// update cache with new results
|
||||||
|
action.data.results.forEach(doc => {
|
||||||
|
state[doc.id] = doc
|
||||||
|
})
|
||||||
|
return { ...state }
|
||||||
|
}
|
||||||
|
|
||||||
|
case actionTypes.DOC_DELETED: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[action.id]: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/client/stores/docsStore.js
Normal file
51
src/client/stores/docsStore.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import actionTypes from '../actionTypes'
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
hasMore: false,
|
||||||
|
pending: false,
|
||||||
|
fetchIdx: 0,
|
||||||
|
error: null,
|
||||||
|
total: null,
|
||||||
|
results: [],
|
||||||
|
page: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function docs(state = initialState, action) {
|
||||||
|
if (
|
||||||
|
action.type !== actionTypes.DOCS_PENDING &&
|
||||||
|
action.fetchIdx !== state.fetchIdx
|
||||||
|
) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case actionTypes.DOCS_PENDING: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: null,
|
||||||
|
pending: true,
|
||||||
|
fetchIdx: action.fetchIdx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case actionTypes.DOCS_LOADED: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...action.data,
|
||||||
|
pending: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case actionTypes.DOCS_ERROR: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pending: false,
|
||||||
|
error: action.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/client/stores/userStore.js
Normal file
28
src/client/stores/userStore.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import actionTypes from '../actionTypes'
|
||||||
|
import config from '../../util/pubConfig'
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
id: null,
|
||||||
|
email: null,
|
||||||
|
admin: null,
|
||||||
|
doSetup: false,
|
||||||
|
verified: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function user(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case actionTypes.SET_USER: {
|
||||||
|
if (state.doSetup && !action.user.doSetup) {
|
||||||
|
config.doSetup = false
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...initialState,
|
||||||
|
...action.user,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
488
src/client/styles/codemirror/codemirror.js
Normal file
488
src/client/styles/codemirror/codemirror.js
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function codemirror(props) {
|
||||||
|
return (
|
||||||
|
<style jsx global>{`
|
||||||
|
/* BASICS */
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
/* Set height, width, borders, and global font properties here */
|
||||||
|
font-family: monospace;
|
||||||
|
height: 300px;
|
||||||
|
color: black;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PADDING */
|
||||||
|
|
||||||
|
.CodeMirror-lines {
|
||||||
|
padding: 4px 0; /* Vertical padding around content */
|
||||||
|
}
|
||||||
|
.CodeMirror pre {
|
||||||
|
padding: 0 4px; /* Horizontal padding of content */
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-scrollbar-filler,
|
||||||
|
.CodeMirror-gutter-filler {
|
||||||
|
background-color: white; /* The little square between H and V scrollbars */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GUTTER */
|
||||||
|
|
||||||
|
.CodeMirror-gutters {
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.CodeMirror-linenumbers {
|
||||||
|
}
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
padding: 0 3px 0 5px;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: right;
|
||||||
|
color: #999;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-guttermarker {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.CodeMirror-guttermarker-subtle {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CURSOR */
|
||||||
|
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
border-left: 1px solid black;
|
||||||
|
border-right: none;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
/* Shown when moving in bi-directional text */
|
||||||
|
.CodeMirror div.CodeMirror-secondarycursor {
|
||||||
|
border-left: 1px solid silver;
|
||||||
|
}
|
||||||
|
.cm-fat-cursor .CodeMirror-cursor {
|
||||||
|
width: auto;
|
||||||
|
border: 0 !important;
|
||||||
|
background: #7e7;
|
||||||
|
}
|
||||||
|
.cm-fat-cursor div.CodeMirror-cursors {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.cm-fat-cursor-mark {
|
||||||
|
background-color: rgba(20, 255, 20, 0.5);
|
||||||
|
-webkit-animation: blink 1.06s steps(1) infinite;
|
||||||
|
-moz-animation: blink 1.06s steps(1) infinite;
|
||||||
|
animation: blink 1.06s steps(1) infinite;
|
||||||
|
}
|
||||||
|
.cm-animate-fat-cursor {
|
||||||
|
width: auto;
|
||||||
|
border: 0;
|
||||||
|
-webkit-animation: blink 1.06s steps(1) infinite;
|
||||||
|
-moz-animation: blink 1.06s steps(1) infinite;
|
||||||
|
animation: blink 1.06s steps(1) infinite;
|
||||||
|
background-color: #7e7;
|
||||||
|
}
|
||||||
|
@-moz-keyframes blink {
|
||||||
|
0% {
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes blink {
|
||||||
|
0% {
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes blink {
|
||||||
|
0% {
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-tab {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-rulers {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: -50px;
|
||||||
|
bottom: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.CodeMirror-ruler {
|
||||||
|
border-left: 1px solid #ccc;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DEFAULT THEME */
|
||||||
|
|
||||||
|
.cm-s-default .cm-header {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-quote {
|
||||||
|
color: #090;
|
||||||
|
}
|
||||||
|
.cm-negative {
|
||||||
|
color: #d44;
|
||||||
|
}
|
||||||
|
.cm-positive {
|
||||||
|
color: #292;
|
||||||
|
}
|
||||||
|
.cm-header,
|
||||||
|
.cm-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.cm-em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.cm-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.cm-strikethrough {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-keyword {
|
||||||
|
color: #708;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-atom {
|
||||||
|
color: #219;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-number {
|
||||||
|
color: #164;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-def {
|
||||||
|
color: #00f;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-variable,
|
||||||
|
.cm-s-default .cm-punctuation,
|
||||||
|
.cm-s-default .cm-property,
|
||||||
|
.cm-s-default .cm-operator {
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-variable-2 {
|
||||||
|
color: #05a;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-variable-3,
|
||||||
|
.cm-s-default .cm-type {
|
||||||
|
color: #085;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-comment {
|
||||||
|
color: #a50;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-string {
|
||||||
|
color: #a11;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-string-2 {
|
||||||
|
color: #f50;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-meta {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-qualifier {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-builtin {
|
||||||
|
color: #30a;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-bracket {
|
||||||
|
color: #997;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-tag {
|
||||||
|
color: #170;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-attribute {
|
||||||
|
color: #00c;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-hr {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.cm-s-default .cm-link {
|
||||||
|
color: #00c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-default .cm-error {
|
||||||
|
color: #f00;
|
||||||
|
}
|
||||||
|
.cm-invalidchar {
|
||||||
|
color: #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-composing {
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default styles for common addons */
|
||||||
|
|
||||||
|
div.CodeMirror span.CodeMirror-matchingbracket {
|
||||||
|
color: #0b0;
|
||||||
|
}
|
||||||
|
div.CodeMirror span.CodeMirror-nonmatchingbracket {
|
||||||
|
color: #a22;
|
||||||
|
}
|
||||||
|
.CodeMirror-matchingtag {
|
||||||
|
background: rgba(255, 150, 0, 0.3);
|
||||||
|
}
|
||||||
|
.CodeMirror-activeline-background {
|
||||||
|
background: #e8f2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* STOP */
|
||||||
|
|
||||||
|
/* The rest of this file contains styles related to the mechanics of
|
||||||
|
the editor. You probably shouldn't touch them. */
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-scroll {
|
||||||
|
overflow: scroll !important; /* Things will break if this is overridden */
|
||||||
|
/* 30px is the magic margin used to hide the element's real scrollbars */
|
||||||
|
/* See overflow: hidden in .CodeMirror */
|
||||||
|
margin-bottom: -30px;
|
||||||
|
margin-right: -30px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
height: 100%;
|
||||||
|
outline: none; /* Prevent dragging from highlighting the element */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.CodeMirror-sizer {
|
||||||
|
position: relative;
|
||||||
|
border-right: 30px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||||
|
before actual scrolling happens, thus preventing shaking and
|
||||||
|
flickering artifacts. */
|
||||||
|
.CodeMirror-vscrollbar,
|
||||||
|
.CodeMirror-hscrollbar,
|
||||||
|
.CodeMirror-scrollbar-filler,
|
||||||
|
.CodeMirror-gutter-filler {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 6;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.CodeMirror-vscrollbar {
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
.CodeMirror-hscrollbar {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
.CodeMirror-scrollbar-filler {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-filler {
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-gutters {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter {
|
||||||
|
white-space: normal;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-bottom: -30px;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
background: none !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-elt {
|
||||||
|
position: absolute;
|
||||||
|
cursor: default;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-wrapper ::selection {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-wrapper ::-moz-selection {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lines {
|
||||||
|
cursor: text;
|
||||||
|
min-height: 1px; /* prevents collapsing before first draw */
|
||||||
|
}
|
||||||
|
.CodeMirror pre {
|
||||||
|
/* Reset some styles that the rest of the page might have set */
|
||||||
|
-moz-border-radius: 0;
|
||||||
|
-webkit-border-radius: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
border-width: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
-webkit-font-variant-ligatures: contextual;
|
||||||
|
font-variant-ligatures: contextual;
|
||||||
|
}
|
||||||
|
.CodeMirror-wrap pre {
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linebackground {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linewidget {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 0.1px; /* Force widget margins to stay inside of the container */
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-widget {
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-rtl pre {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-code {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force content-box sizing for the elements where we expect it */
|
||||||
|
.CodeMirror-scroll,
|
||||||
|
.CodeMirror-sizer,
|
||||||
|
.CodeMirror-gutter,
|
||||||
|
.CodeMirror-gutters,
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-measure {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.CodeMirror-measure pre {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.CodeMirror-cursors {
|
||||||
|
visibility: hidden;
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
div.CodeMirror-dragcursors {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-focused div.CodeMirror-cursors {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-selected {
|
||||||
|
background: #d9d9d9;
|
||||||
|
}
|
||||||
|
.CodeMirror-focused .CodeMirror-selected {
|
||||||
|
background: #d7d4f0;
|
||||||
|
}
|
||||||
|
.CodeMirror-crosshair {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.CodeMirror-line::selection,
|
||||||
|
.CodeMirror-line > span::selection,
|
||||||
|
.CodeMirror-line > span > span::selection {
|
||||||
|
background: #d7d4f0;
|
||||||
|
}
|
||||||
|
.CodeMirror-line::-moz-selection,
|
||||||
|
.CodeMirror-line > span::-moz-selection,
|
||||||
|
.CodeMirror-line > span > span::-moz-selection {
|
||||||
|
background: #d7d4f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-searching {
|
||||||
|
background-color: #ffa;
|
||||||
|
background-color: rgba(255, 255, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Used to force a border model for a node */
|
||||||
|
.cm-force-border {
|
||||||
|
padding-right: 0.1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Hide the cursor when printing */
|
||||||
|
.CodeMirror div.CodeMirror-cursors {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* See issue #2901 */
|
||||||
|
.cm-tab-wrap-hack:after {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Help users use markselection to safely style text background */
|
||||||
|
span.CodeMirror-selectedtext {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
)
|
||||||
|
}
|
||||||
117
src/client/styles/codemirror/monokai.js
Normal file
117
src/client/styles/codemirror/monokai.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function monokai(props) {
|
||||||
|
return (
|
||||||
|
<style jsx global>{`
|
||||||
|
/* Based on Sublime Text's Monokai theme */
|
||||||
|
.cm-s-monokai.CodeMirror {
|
||||||
|
background: #272822;
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
.cm-s-monokai div.CodeMirror-selected {
|
||||||
|
background: #49483e;
|
||||||
|
}
|
||||||
|
.cm-s-monokai .CodeMirror-line::selection,
|
||||||
|
.cm-s-monokai .CodeMirror-line > span::selection,
|
||||||
|
.cm-s-monokai .CodeMirror-line > span > span::selection {
|
||||||
|
background: rgba(73, 72, 62, 0.99);
|
||||||
|
}
|
||||||
|
.cm-s-monokai .CodeMirror-line::-moz-selection,
|
||||||
|
.cm-s-monokai .CodeMirror-line > span::-moz-selection,
|
||||||
|
.cm-s-monokai .CodeMirror-line > span > span::-moz-selection {
|
||||||
|
background: rgba(73, 72, 62, 0.99);
|
||||||
|
}
|
||||||
|
.cm-s-monokai .CodeMirror-gutters {
|
||||||
|
background: #272822;
|
||||||
|
border-right: 0px;
|
||||||
|
}
|
||||||
|
.cm-s-monokai .CodeMirror-guttermarker {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.cm-s-monokai .CodeMirror-guttermarker-subtle {
|
||||||
|
color: #d0d0d0;
|
||||||
|
}
|
||||||
|
.cm-s-monokai .CodeMirror-linenumber {
|
||||||
|
color: #d0d0d0;
|
||||||
|
}
|
||||||
|
.cm-s-monokai .CodeMirror-cursor {
|
||||||
|
border-left: 1px solid #f8f8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-monokai span.cm-comment {
|
||||||
|
color: #75715e;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-atom {
|
||||||
|
color: #ae81ff;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-number {
|
||||||
|
color: #ae81ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-monokai span.cm-comment.cm-attribute {
|
||||||
|
color: #97b757;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-comment.cm-def {
|
||||||
|
color: #bc9262;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-comment.cm-tag {
|
||||||
|
color: #bc6283;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-comment.cm-type {
|
||||||
|
color: #5998a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-monokai span.cm-property,
|
||||||
|
.cm-s-monokai span.cm-attribute {
|
||||||
|
color: #a6e22e;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-keyword {
|
||||||
|
color: #f92672;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-builtin {
|
||||||
|
color: #66d9ef;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-string {
|
||||||
|
color: #e6db74;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-monokai span.cm-variable {
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-variable-2 {
|
||||||
|
color: #9effff;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-variable-3,
|
||||||
|
.cm-s-monokai span.cm-type {
|
||||||
|
color: #66d9ef;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-def {
|
||||||
|
color: #fd971f;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-bracket {
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-tag {
|
||||||
|
color: #f92672;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-header {
|
||||||
|
color: #ae81ff;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-link {
|
||||||
|
color: #ae81ff;
|
||||||
|
}
|
||||||
|
.cm-s-monokai span.cm-error {
|
||||||
|
background: #f92672;
|
||||||
|
color: #f8f8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-monokai .CodeMirror-activeline-background {
|
||||||
|
background: #373831;
|
||||||
|
}
|
||||||
|
.cm-s-monokai .CodeMirror-matchingbracket {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
)
|
||||||
|
}
|
||||||
138
src/client/styles/global.js
Normal file
138
src/client/styles/global.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import theme from '../theme'
|
||||||
|
|
||||||
|
export default function global(props) {
|
||||||
|
return (
|
||||||
|
<style jsx global>{`
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, code, pre {
|
||||||
|
background: ${theme.primary};
|
||||||
|
color: ${theme.text};
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre, code {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea, select, button, .button, .cm-s-monokai.CodeMirror {
|
||||||
|
color: ${theme.text};
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: none !important;
|
||||||
|
background-color ${theme.primaryAlt} !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[disabled] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-family: ${theme.fontFamily};
|
||||||
|
font-weight: 300;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[disabled], textarea[disabled] {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder, textarea::placeholder {
|
||||||
|
opacity: 0.85;
|
||||||
|
color: ${theme.text};
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%23d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus {
|
||||||
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%239b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: ${theme.link};
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: ${theme.link};
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus, a:hover {
|
||||||
|
color: ${theme.linkAct};
|
||||||
|
}
|
||||||
|
|
||||||
|
.Markdown pre {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
/* 55px: header height, 50px: footer height */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 55px - 51px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main > .padded.container {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
color: ${theme.danger};
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginate {
|
||||||
|
list-style: none;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginate li {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginate li.active a {
|
||||||
|
border-color: ${theme.link};
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginate a {
|
||||||
|
outline: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: transparent;
|
||||||
|
padding: 3px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 840px) {
|
||||||
|
.row .column.column-10,
|
||||||
|
.row .column.column-20,
|
||||||
|
.row .column.column-25,
|
||||||
|
.row .column.column-33,
|
||||||
|
.row .column.column-40,
|
||||||
|
.row .column.column-50,
|
||||||
|
.row .column.column-60,
|
||||||
|
.row .column.column-67,
|
||||||
|
.row .column.column-75,
|
||||||
|
.row .column.column-80,
|
||||||
|
.row .column.column-90 {
|
||||||
|
flex: 1 1 auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
)
|
||||||
|
}
|
||||||
539
src/client/styles/milligram.js
Normal file
539
src/client/styles/milligram.js
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function milligram(props) {
|
||||||
|
return (
|
||||||
|
<style jsx global>{`
|
||||||
|
*,
|
||||||
|
*:after,
|
||||||
|
*:before {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 62.5%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
color: #606c76;
|
||||||
|
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 1.6em;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
border-left: 0.3rem solid #d1d1d1;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
blockquote *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.button,
|
||||||
|
button,
|
||||||
|
input[type='button'],
|
||||||
|
input[type='reset'],
|
||||||
|
input[type='submit'] {
|
||||||
|
background-color: #9b4dca;
|
||||||
|
border: 0.1rem solid #9b4dca;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
height: 3.8rem;
|
||||||
|
letter-spacing: 0.1rem;
|
||||||
|
line-height: 3.8rem;
|
||||||
|
padding: 0 3rem;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.button:focus,
|
||||||
|
.button:hover,
|
||||||
|
button:focus,
|
||||||
|
button:hover,
|
||||||
|
input[type='button']:focus,
|
||||||
|
input[type='button']:hover,
|
||||||
|
input[type='reset']:focus,
|
||||||
|
input[type='reset']:hover,
|
||||||
|
input[type='submit']:focus,
|
||||||
|
input[type='submit']:hover {
|
||||||
|
background-color: #606c76;
|
||||||
|
border-color: #606c76;
|
||||||
|
color: #fff;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
.button[disabled],
|
||||||
|
button[disabled],
|
||||||
|
input[type='button'][disabled],
|
||||||
|
input[type='reset'][disabled],
|
||||||
|
input[type='submit'][disabled] {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.button[disabled]:focus,
|
||||||
|
.button[disabled]:hover,
|
||||||
|
button[disabled]:focus,
|
||||||
|
button[disabled]:hover,
|
||||||
|
input[type='button'][disabled]:focus,
|
||||||
|
input[type='button'][disabled]:hover,
|
||||||
|
input[type='reset'][disabled]:focus,
|
||||||
|
input[type='reset'][disabled]:hover,
|
||||||
|
input[type='submit'][disabled]:focus,
|
||||||
|
input[type='submit'][disabled]:hover {
|
||||||
|
background-color: #9b4dca;
|
||||||
|
border-color: #9b4dca;
|
||||||
|
}
|
||||||
|
.button.button-outline,
|
||||||
|
button.button-outline,
|
||||||
|
input[type='button'].button-outline,
|
||||||
|
input[type='reset'].button-outline,
|
||||||
|
input[type='submit'].button-outline {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
.button.button-outline:focus,
|
||||||
|
.button.button-outline:hover,
|
||||||
|
button.button-outline:focus,
|
||||||
|
button.button-outline:hover,
|
||||||
|
input[type='button'].button-outline:focus,
|
||||||
|
input[type='button'].button-outline:hover,
|
||||||
|
input[type='reset'].button-outline:focus,
|
||||||
|
input[type='reset'].button-outline:hover,
|
||||||
|
input[type='submit'].button-outline:focus,
|
||||||
|
input[type='submit'].button-outline:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: #606c76;
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
.button.button-outline[disabled]:focus,
|
||||||
|
.button.button-outline[disabled]:hover,
|
||||||
|
button.button-outline[disabled]:focus,
|
||||||
|
button.button-outline[disabled]:hover,
|
||||||
|
input[type='button'].button-outline[disabled]:focus,
|
||||||
|
input[type='button'].button-outline[disabled]:hover,
|
||||||
|
input[type='reset'].button-outline[disabled]:focus,
|
||||||
|
input[type='reset'].button-outline[disabled]:hover,
|
||||||
|
input[type='submit'].button-outline[disabled]:focus,
|
||||||
|
input[type='submit'].button-outline[disabled]:hover {
|
||||||
|
border-color: inherit;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
.button.button-clear,
|
||||||
|
button.button-clear,
|
||||||
|
input[type='button'].button-clear,
|
||||||
|
input[type='reset'].button-clear,
|
||||||
|
input[type='submit'].button-clear {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
.button.button-clear:focus,
|
||||||
|
.button.button-clear:hover,
|
||||||
|
button.button-clear:focus,
|
||||||
|
button.button-clear:hover,
|
||||||
|
input[type='button'].button-clear:focus,
|
||||||
|
input[type='button'].button-clear:hover,
|
||||||
|
input[type='reset'].button-clear:focus,
|
||||||
|
input[type='reset'].button-clear:hover,
|
||||||
|
input[type='submit'].button-clear:focus,
|
||||||
|
input[type='submit'].button-clear:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
.button.button-clear[disabled]:focus,
|
||||||
|
.button.button-clear[disabled]:hover,
|
||||||
|
button.button-clear[disabled]:focus,
|
||||||
|
button.button-clear[disabled]:hover,
|
||||||
|
input[type='button'].button-clear[disabled]:focus,
|
||||||
|
input[type='button'].button-clear[disabled]:hover,
|
||||||
|
input[type='reset'].button-clear[disabled]:focus,
|
||||||
|
input[type='reset'].button-clear[disabled]:hover,
|
||||||
|
input[type='submit'].button-clear[disabled]:focus,
|
||||||
|
input[type='submit'].button-clear[disabled]:hover {
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: #f4f5f6;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
font-size: 86%;
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #f4f5f6;
|
||||||
|
border-left: 0.3rem solid #9b4dca;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
pre > code {
|
||||||
|
border-radius: 0;
|
||||||
|
display: block;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 0.1rem solid #f4f5f6;
|
||||||
|
margin: 3rem 0;
|
||||||
|
}
|
||||||
|
input[type='email'],
|
||||||
|
input[type='number'],
|
||||||
|
input[type='password'],
|
||||||
|
input[type='search'],
|
||||||
|
input[type='tel'],
|
||||||
|
input[type='text'],
|
||||||
|
input[type='url'],
|
||||||
|
input[type='color'],
|
||||||
|
input[type='date'],
|
||||||
|
input[type='month'],
|
||||||
|
input[type='week'],
|
||||||
|
input[type='datetime'],
|
||||||
|
input[type='datetime-local'],
|
||||||
|
input:not([type]),
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0.1rem solid #d1d1d1;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
box-shadow: none;
|
||||||
|
box-sizing: inherit;
|
||||||
|
height: 3.8rem;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
input[type='email']:focus,
|
||||||
|
input[type='number']:focus,
|
||||||
|
input[type='password']:focus,
|
||||||
|
input[type='search']:focus,
|
||||||
|
input[type='tel']:focus,
|
||||||
|
input[type='text']:focus,
|
||||||
|
input[type='url']:focus,
|
||||||
|
input[type='color']:focus,
|
||||||
|
input[type='date']:focus,
|
||||||
|
input[type='month']:focus,
|
||||||
|
input[type='week']:focus,
|
||||||
|
input[type='datetime']:focus,
|
||||||
|
input[type='datetime-local']:focus,
|
||||||
|
input:not([type]):focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
border-color: #9b4dca;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%23d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>')
|
||||||
|
center right no-repeat;
|
||||||
|
padding-right: 3rem;
|
||||||
|
}
|
||||||
|
select:focus {
|
||||||
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%239b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
min-height: 6.5rem;
|
||||||
|
}
|
||||||
|
label,
|
||||||
|
legend {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
fieldset {
|
||||||
|
border-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
input[type='checkbox'],
|
||||||
|
input[type='radio'] {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.label-inline {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 112rem;
|
||||||
|
padding: 0 2rem;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.row.row-no-padding {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.row.row-no-padding > .column {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.row.row-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.row.row-top {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.row.row-bottom {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.row.row-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.row.row-stretch {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.row.row-baseline {
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
.row .column {
|
||||||
|
display: block;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin-left: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.row .column.column-offset-10 {
|
||||||
|
margin-left: 10%;
|
||||||
|
}
|
||||||
|
.row .column.column-offset-20 {
|
||||||
|
margin-left: 20%;
|
||||||
|
}
|
||||||
|
.row .column.column-offset-25 {
|
||||||
|
margin-left: 25%;
|
||||||
|
}
|
||||||
|
.row .column.column-offset-33,
|
||||||
|
.row .column.column-offset-34 {
|
||||||
|
margin-left: 33.3333%;
|
||||||
|
}
|
||||||
|
.row .column.column-offset-50 {
|
||||||
|
margin-left: 50%;
|
||||||
|
}
|
||||||
|
.row .column.column-offset-66,
|
||||||
|
.row .column.column-offset-67 {
|
||||||
|
margin-left: 66.6666%;
|
||||||
|
}
|
||||||
|
.row .column.column-offset-75 {
|
||||||
|
margin-left: 75%;
|
||||||
|
}
|
||||||
|
.row .column.column-offset-80 {
|
||||||
|
margin-left: 80%;
|
||||||
|
}
|
||||||
|
.row .column.column-offset-90 {
|
||||||
|
margin-left: 90%;
|
||||||
|
}
|
||||||
|
.row .column.column-10 {
|
||||||
|
flex: 0 0 10%;
|
||||||
|
max-width: 10%;
|
||||||
|
}
|
||||||
|
.row .column.column-20 {
|
||||||
|
flex: 0 0 20%;
|
||||||
|
max-width: 20%;
|
||||||
|
}
|
||||||
|
.row .column.column-25 {
|
||||||
|
flex: 0 0 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
|
.row .column.column-33,
|
||||||
|
.row .column.column-34 {
|
||||||
|
flex: 0 0 33.3333%;
|
||||||
|
max-width: 33.3333%;
|
||||||
|
}
|
||||||
|
.row .column.column-40 {
|
||||||
|
flex: 0 0 40%;
|
||||||
|
max-width: 40%;
|
||||||
|
}
|
||||||
|
.row .column.column-50 {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
.row .column.column-60 {
|
||||||
|
flex: 0 0 60%;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
.row .column.column-66,
|
||||||
|
.row .column.column-67 {
|
||||||
|
flex: 0 0 66.6666%;
|
||||||
|
max-width: 66.6666%;
|
||||||
|
}
|
||||||
|
.row .column.column-75 {
|
||||||
|
flex: 0 0 75%;
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
.row .column.column-80 {
|
||||||
|
flex: 0 0 80%;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
.row .column.column-90 {
|
||||||
|
flex: 0 0 90%;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
.row .column .column-top {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
.row .column .column-bottom {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
.row .column .column-center {
|
||||||
|
-ms-grid-row-align: center;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
@media (min-width: 40rem) {
|
||||||
|
.row {
|
||||||
|
flex-direction: row;
|
||||||
|
margin-left: -1rem;
|
||||||
|
width: calc(100% + 2rem);
|
||||||
|
}
|
||||||
|
.row .column {
|
||||||
|
margin-bottom: inherit;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #9b4dca;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:focus,
|
||||||
|
a:hover {
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
dl,
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
dl dl,
|
||||||
|
dl ol,
|
||||||
|
dl ul,
|
||||||
|
ol dl,
|
||||||
|
ol ol,
|
||||||
|
ol ul,
|
||||||
|
ul dl,
|
||||||
|
ul ol,
|
||||||
|
ul ul {
|
||||||
|
font-size: 90%;
|
||||||
|
margin: 1.5rem 0 1.5rem 3rem;
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
list-style: decimal inside;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style: circle inside;
|
||||||
|
}
|
||||||
|
.button,
|
||||||
|
button,
|
||||||
|
dd,
|
||||||
|
dt,
|
||||||
|
li {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
fieldset,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
blockquote,
|
||||||
|
dl,
|
||||||
|
figure,
|
||||||
|
form,
|
||||||
|
ol,
|
||||||
|
p,
|
||||||
|
pre,
|
||||||
|
table,
|
||||||
|
ul {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-bottom: 0.1rem solid #e1e1e1;
|
||||||
|
padding: 1.2rem 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
td:first-child,
|
||||||
|
th:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
td:last-child,
|
||||||
|
th:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: -0.1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 4.6rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
letter-spacing: -0.08rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
letter-spacing: -0.05rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
h6 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.clearfix:after {
|
||||||
|
clear: both;
|
||||||
|
content: ' ';
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
.float-left {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.float-right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
)
|
||||||
|
}
|
||||||
304
src/client/styles/roboto.js
Normal file
304
src/client/styles/roboto.js
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function roboto(props) {
|
||||||
|
return (
|
||||||
|
<style jsx global>{`
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc3CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF,
|
||||||
|
U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc-CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc2CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc5CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc1CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc0CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
|
||||||
|
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc6CsTYl4BO.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
|
||||||
|
U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||||
|
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic3CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF,
|
||||||
|
U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic-CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic2CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic5CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic1CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic0CsTYl4BOQ3o.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
|
||||||
|
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic6CsTYl4BO.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
|
||||||
|
U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||||
|
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fCRc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF,
|
||||||
|
U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fABc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fCBc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fBxc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fCxc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fChc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
|
||||||
|
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fBBc4AMP6lQ.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
|
||||||
|
U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||||
|
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold'), local('Roboto-Bold'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfCRc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF,
|
||||||
|
U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold'), local('Roboto-Bold'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfABc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold'), local('Roboto-Bold'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfCBc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold'), local('Roboto-Bold'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfBxc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold'), local('Roboto-Bold'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfCxc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold'), local('Roboto-Bold'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfChc4AMP6lbBP.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
|
||||||
|
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Roboto Bold'), local('Roboto-Bold'),
|
||||||
|
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lQ.woff2)
|
||||||
|
format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
|
||||||
|
U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||||
|
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
)
|
||||||
|
}
|
||||||
50
src/client/util/checkLogin.js
Normal file
50
src/client/util/checkLogin.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { getStore } from '../store'
|
||||||
|
import actionTypes from '../actionTypes'
|
||||||
|
import addBase from '../../util/addBase'
|
||||||
|
import config from '../../util/pubConfig'
|
||||||
|
|
||||||
|
export default async function checkLogin(req) {
|
||||||
|
let user = req && req.user
|
||||||
|
if (user) {
|
||||||
|
user = {
|
||||||
|
...user,
|
||||||
|
jwt: req.cookies.jwt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const store = getStore()
|
||||||
|
|
||||||
|
if (!user && !config.ssr && !store.getState().user.verified) {
|
||||||
|
const jwt = localStorage.jwt
|
||||||
|
|
||||||
|
if (jwt) {
|
||||||
|
const jwt = localStorage.jwt
|
||||||
|
await fetch(addBase('/auth'), {
|
||||||
|
headers: {
|
||||||
|
Authorization: `bearer ${jwt}`,
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
const payload = JSON.parse(atob(jwt.split('.')[1]))
|
||||||
|
user = {
|
||||||
|
...payload.user,
|
||||||
|
verified: true,
|
||||||
|
jwt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user && config.doSetup) {
|
||||||
|
user = { doSetup: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user)
|
||||||
|
store.dispatch({
|
||||||
|
type: actionTypes.SET_USER,
|
||||||
|
user,
|
||||||
|
})
|
||||||
|
}
|
||||||
63
src/client/util/docHelpers.js
Normal file
63
src/client/util/docHelpers.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { getStore } from '../store'
|
||||||
|
import Router from 'next/router'
|
||||||
|
import getHeaders from './getHeaders'
|
||||||
|
import actionTypes from '../actionTypes'
|
||||||
|
import addBase from '../../util/addBase'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* delete doc
|
||||||
|
* @param { String } id - id of doc to delete
|
||||||
|
* @returns { Promise }
|
||||||
|
*/
|
||||||
|
export const deleteDoc = (id, confirm = true) => {
|
||||||
|
if (confirm && !window.confirm('Are you sure you want to delete this doc?')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return fetch(addBase(`/docs?id=${id}`), {
|
||||||
|
headers: getHeaders(),
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
Router.push('/', addBase('/')).then(() => {
|
||||||
|
getStore().dispatch({
|
||||||
|
type: actionTypes.DOC_DELETED,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
alert('Error occurred deleting doc: ', err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param { String|undefined } id - id of doc if update or undefined if new
|
||||||
|
* @param { String } md - the documents markdown
|
||||||
|
* @param { String } name - name of document
|
||||||
|
* @param { String } dir - sub-dir of docsDir for document
|
||||||
|
* @returns { Promise }
|
||||||
|
*/
|
||||||
|
export const updateDoc = (id, md, name, dir) => {
|
||||||
|
const method = id ? 'PATCH' : 'POST'
|
||||||
|
const query = id ? `?id=${id}` : ''
|
||||||
|
const data = {}
|
||||||
|
if (md) data.md = md
|
||||||
|
if (name) data.name = name
|
||||||
|
if (typeof dir === 'string') data.dir = dir
|
||||||
|
if (name && name.slice(-3) !== '.md') data.name += '.md'
|
||||||
|
|
||||||
|
return fetch(addBase(`/docs${query}`), {
|
||||||
|
headers: {
|
||||||
|
...getHeaders(),
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
method,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}).then(async res => {
|
||||||
|
const { id, ...data } = await res.json()
|
||||||
|
if (!id) throw new Error(data.message || 'error occurred adding doc')
|
||||||
|
const docUrl = `/doc?id=${id}`
|
||||||
|
Router.push(docUrl, addBase(docUrl))
|
||||||
|
})
|
||||||
|
}
|
||||||
15
src/client/util/getHeaders.js
Normal file
15
src/client/util/getHeaders.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { getStore } from '../store'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets headers for xhr request
|
||||||
|
* @returns { Object } the headers
|
||||||
|
*/
|
||||||
|
export default function getHeaders() {
|
||||||
|
const { jwt } = getStore().getState().user
|
||||||
|
|
||||||
|
return !jwt
|
||||||
|
? false
|
||||||
|
: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/client/util/loadDocs.js
Normal file
92
src/client/util/loadDocs.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { format } from 'url'
|
||||||
|
import { getStore } from '../store'
|
||||||
|
import getHeaders from './getHeaders'
|
||||||
|
import fetch from 'isomorphic-unfetch'
|
||||||
|
import addBase from '../../util/addBase'
|
||||||
|
import actionTypes from '../actionTypes'
|
||||||
|
import config from '../../util/pubConfig'
|
||||||
|
import {
|
||||||
|
buildSortBy,
|
||||||
|
limitDocs,
|
||||||
|
searchDocs,
|
||||||
|
sortDocs,
|
||||||
|
} from '../../util/kbHelpers'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loads docs
|
||||||
|
* @param { Object } query - docs query object
|
||||||
|
* @param { Boolean } forCache - whether this is meant specifically for cache
|
||||||
|
* @returns { AbortController } instance of abort controller if supported
|
||||||
|
*/
|
||||||
|
export default function loadDocs(query, forCache) {
|
||||||
|
const queryStr = format({ query })
|
||||||
|
const store = getStore()
|
||||||
|
const url = addBase(`/docs${queryStr}`, true)
|
||||||
|
const headers = getHeaders()
|
||||||
|
|
||||||
|
if (!headers) return
|
||||||
|
const { docs, cache } = store.getState()
|
||||||
|
const fetchIdx = docs.fetchIdx + 1
|
||||||
|
store.dispatch({ type: actionTypes.DOCS_PENDING, fetchIdx })
|
||||||
|
|
||||||
|
let controller
|
||||||
|
let signal
|
||||||
|
|
||||||
|
if (!config.ssr && 'AbortController' in window) {
|
||||||
|
controller = new AbortController()
|
||||||
|
signal = controller.signal
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = fetch(url, { headers, signal })
|
||||||
|
.then(async res => {
|
||||||
|
let data = await res.json()
|
||||||
|
if (res.status !== 200) throw new Error(data.message)
|
||||||
|
if (data.id) data = { results: [data] }
|
||||||
|
store.dispatch({
|
||||||
|
type: forCache ? actionTypes.CACHE_DOCS : actionTypes.DOCS_LOADED,
|
||||||
|
fetchIdx,
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
page: query.page || 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.name === 'AbortError') return
|
||||||
|
// try cached docs if offline
|
||||||
|
if (err.message === 'Failed to fetch') {
|
||||||
|
// handle fetching from cache
|
||||||
|
let cacheDocs
|
||||||
|
if (query.search) cacheDocs = searchDocs(cache, query.search)
|
||||||
|
cacheDocs = sortDocs(
|
||||||
|
cache,
|
||||||
|
buildSortBy(query),
|
||||||
|
cacheDocs && cacheDocs.map(doc => doc.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch({
|
||||||
|
type: actionTypes.DOCS_LOADED,
|
||||||
|
fetchIdx,
|
||||||
|
data: {
|
||||||
|
...limitDocs(
|
||||||
|
cacheDocs,
|
||||||
|
query,
|
||||||
|
config.defDocsLimit,
|
||||||
|
config.maxDocsLimit
|
||||||
|
),
|
||||||
|
page: query.page || 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
store.dispatch({
|
||||||
|
type: actionTypes.DOCS_ERROR,
|
||||||
|
error: err.message,
|
||||||
|
fetchIdx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (config.ssr) return req
|
||||||
|
// return controller to abort request
|
||||||
|
return controller
|
||||||
|
}
|
||||||
17
src/client/util/logout.js
Normal file
17
src/client/util/logout.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { getStore } from '../store'
|
||||||
|
import addBase from '../../util/addBase'
|
||||||
|
import actionTypes from '../actionTypes'
|
||||||
|
|
||||||
|
export default function logout(e) {
|
||||||
|
e && e.preventDefault()
|
||||||
|
delete localStorage.jwt
|
||||||
|
|
||||||
|
fetch(addBase('/logout'), {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(() => {
|
||||||
|
getStore().dispatch({
|
||||||
|
type: actionTypes.USER_LOGOUT,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
37
src/client/util/offlineFallback.js
Normal file
37
src/client/util/offlineFallback.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const basePath = new URL(location).searchParams.get('basePath')
|
||||||
|
const offlinePath = basePath + 'offline'
|
||||||
|
|
||||||
|
if (basePath !== '/') {
|
||||||
|
const curBase =
|
||||||
|
basePath.slice(-1) === '/'
|
||||||
|
? basePath.substr(0, basePath.length - 1)
|
||||||
|
: basePath
|
||||||
|
for (let item of self.precacheConfig) {
|
||||||
|
item[0] = curBase + item[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
var offlineRequest = new Request(offlinePath)
|
||||||
|
event.waitUntil(
|
||||||
|
fetch(offlineRequest).then(response => {
|
||||||
|
return caches.open('offline').then(cache => {
|
||||||
|
return cache.put(offlineRequest, response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
const { request } = event
|
||||||
|
const { method, headers } = request
|
||||||
|
if (method === 'GET' && headers.get('accept').includes('text/html')) {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(request).catch(err => {
|
||||||
|
return caches.open('offline').then(cache => {
|
||||||
|
return cache.match(offlinePath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
8
src/client/util/registerServiceWorker.js
Normal file
8
src/client/util/registerServiceWorker.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import addBase from '../../util/addBase'
|
||||||
|
import config from '../../util/pubConfig'
|
||||||
|
;(function() {
|
||||||
|
if (!config.dev && !config.ssr && 'serviceWorker' in navigator) {
|
||||||
|
const basePath = encodeURIComponent(addBase('/'))
|
||||||
|
navigator.serviceWorker.register(addBase(`sw.js?basePath=${basePath}`))
|
||||||
|
}
|
||||||
|
})()
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import fetch from 'isomorphic-unfetch'
|
|
||||||
import mapUser from '../util/mapUser'
|
|
||||||
import getUrl from '../util/getUrl'
|
|
||||||
import getJwt from '../util/getJwt'
|
|
||||||
|
|
||||||
const getDoc = async (id, req) => {
|
|
||||||
let found, doc
|
|
||||||
const jwt = getJwt(req)
|
|
||||||
if (!jwt) return { found, doc, id }
|
|
||||||
const docRes = await fetch(getUrl('docs/' + id, Boolean(req)), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { Authorization: jwt },
|
|
||||||
})
|
|
||||||
if (docRes.ok) {
|
|
||||||
doc = await docRes.json()
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
return { found, doc, id }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ComposedComponent => {
|
|
||||||
class DocComp extends Component {
|
|
||||||
state = {
|
|
||||||
found: false,
|
|
||||||
id: null,
|
|
||||||
doc: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getInitialProps({ query, req }) {
|
|
||||||
return await getDoc(query.id, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
|
||||||
const { found, id, doc } = nextProps
|
|
||||||
if (prevState.found !== found && !prevState.didInit) {
|
|
||||||
return { found, id, doc, didInit: true }
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDoc = async id => {
|
|
||||||
this.setState(await getDoc(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.updateDoc(this.props.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { user, found, id } = this.props
|
|
||||||
if (prevProps.user.email === user.email || found) return
|
|
||||||
if (!user.email) return
|
|
||||||
this.updateDoc(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <ComposedComponent {...this.state} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return connect(mapUser)(DocComp)
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import cm from 'codemirror'
|
|
||||||
import { getKey, isCtrlKey } from '../util/keys'
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
require('codemirror/mode/markdown/markdown')
|
|
||||||
}
|
|
||||||
export default class CodeMirror extends Component {
|
|
||||||
handleChange = () => {
|
|
||||||
if (!this.editor) return
|
|
||||||
const value = this.editor.getValue()
|
|
||||||
if (value !== this.props.value) {
|
|
||||||
this.props.onChange && this.props.onChange(value)
|
|
||||||
if (this.editor.getValue() !== this.props.value) {
|
|
||||||
if (this.state.isControlled) {
|
|
||||||
this.editor.setValue(this.props.value)
|
|
||||||
} else {
|
|
||||||
this.props.value = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkSubmit = (cm, e) => {
|
|
||||||
const key = getKey(e)
|
|
||||||
if (isCtrlKey(key)) {
|
|
||||||
this.ctrlKey = true
|
|
||||||
} else if (key === 13 && this.ctrlKey) {
|
|
||||||
this.props.onSubmit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleKeyUp = (cm, e) => {
|
|
||||||
if (isCtrlKey(getKey(e))) this.ctrlKey = false
|
|
||||||
}
|
|
||||||
componentDidMount() {
|
|
||||||
if (typeof window === 'undefined') return
|
|
||||||
this.editor = cm.fromTextArea(this.textarea, this.props.options)
|
|
||||||
this.editor.on('change', this.handleChange)
|
|
||||||
if (typeof this.props.onSubmit === 'function') {
|
|
||||||
this.editor.on('keydown', this.checkSubmit)
|
|
||||||
this.editor.on('keyup', this.handleKeyUp)
|
|
||||||
this.setupSubmitKey = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.setupSubmitKey) {
|
|
||||||
this.editor.off('keydown', this.checkSubmit)
|
|
||||||
this.editor.off('keyup', this.handleKeyUp)
|
|
||||||
this.setupSubmitKey = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
componentDidUpdate() {
|
|
||||||
if (!this.editor || !this.props.value) return
|
|
||||||
if (this.editor.getValue() !== this.props.value) {
|
|
||||||
this.editor.setValue(this.props.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
const { value, className, onChange } = this.props
|
|
||||||
return (
|
|
||||||
<div {...{ className }}>
|
|
||||||
<textarea {...{ value, onChange }} ref={el => (this.textarea = el)} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
import getUrl from '../util/getUrl'
|
|
||||||
|
|
||||||
const DocItem = ({ id, name, dir, updated }) => {
|
|
||||||
name = dir + (dir.length > 0 ? '/' : '') + name
|
|
||||||
const as = getUrl('k/' + id)
|
|
||||||
const href = { pathname: '/k', query: { id } }
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<Link {...{ href, as }}>
|
|
||||||
<a>
|
|
||||||
<p className="noMargin">
|
|
||||||
{name}
|
|
||||||
<span className="float-right">
|
|
||||||
{new Date(updated).toLocaleDateString('en-US')}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DocItem
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { css } from 'glamor'
|
|
||||||
import theme from '../styles/theme'
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '10px 10px 15px',
|
|
||||||
background: theme.primaryAlt,
|
|
||||||
|
|
||||||
'& p': {
|
|
||||||
marginBottom: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const Footer = () => (
|
|
||||||
<footer className={css(style)}>
|
|
||||||
<p>
|
|
||||||
Powered by{' '}
|
|
||||||
<a
|
|
||||||
href="//github.com/ijjk/mykb"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
MYKB
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default Footer
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import { css } from 'glamor'
|
|
||||||
import theme from '../styles/theme'
|
|
||||||
import { withRouter } from 'next/router'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import { doLogout } from '../redux/actions/userAct'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import getUrl from '../util/getUrl'
|
|
||||||
import mapUser from '../util/mapUser'
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
background: theme.primaryAlt,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: 55,
|
|
||||||
|
|
||||||
'& .navbar-brand': {
|
|
||||||
marginLeft: '0.75em',
|
|
||||||
marginRight: 'auto',
|
|
||||||
|
|
||||||
'& h3': {
|
|
||||||
marginBottom: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'& .navbar-burger': {
|
|
||||||
width: 32,
|
|
||||||
display: 'none',
|
|
||||||
marginRight: 10,
|
|
||||||
|
|
||||||
'&.active div': {
|
|
||||||
'&:nth-child(1)': {
|
|
||||||
transformOrigin: 'center',
|
|
||||||
transform: 'translateY(8px) rotate(45deg)',
|
|
||||||
},
|
|
||||||
'&:nth-child(2)': {
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
'&:nth-child(3)': {
|
|
||||||
transformOrigin: 'left -6px',
|
|
||||||
transform: 'translateY(8px) rotate(-45deg)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'& div': {
|
|
||||||
transition: 'all ease-in-out 150ms',
|
|
||||||
width: '100%',
|
|
||||||
height: 2,
|
|
||||||
margin: '5px 0',
|
|
||||||
borderRadius: 1,
|
|
||||||
background: theme.text,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'& .navbar-items': {
|
|
||||||
display: 'inline-flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
|
|
||||||
'& .active .item, .item:hover': {
|
|
||||||
background: theme.primary,
|
|
||||||
},
|
|
||||||
'& .item': {
|
|
||||||
margin: 0,
|
|
||||||
cursor: 'pointer',
|
|
||||||
padding: '15px 20px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'@media screen and (max-width: 840px)': {
|
|
||||||
'& .navbar-burger': {
|
|
||||||
display: 'inline-block',
|
|
||||||
},
|
|
||||||
|
|
||||||
'& .navbar-items': {
|
|
||||||
display: 'block',
|
|
||||||
overflow: 'hidden',
|
|
||||||
position: 'fixed',
|
|
||||||
top: 55,
|
|
||||||
left: 0,
|
|
||||||
zIndex: 5,
|
|
||||||
background: theme.primaryAlt,
|
|
||||||
width: '100%',
|
|
||||||
transform: 'scaleY(0)',
|
|
||||||
transformOrigin: 'top',
|
|
||||||
transition: 'all ease-in-out 125ms',
|
|
||||||
|
|
||||||
'&.active': {
|
|
||||||
transform: 'scaleY(1)',
|
|
||||||
overflow: 'auto',
|
|
||||||
},
|
|
||||||
'& .item': {
|
|
||||||
width: '100%',
|
|
||||||
padding: '5px 0',
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const NavLink = ({ children, href, active }) => {
|
|
||||||
const activeClass = active ? ' active' : ''
|
|
||||||
return (
|
|
||||||
<Link href={href} as={getUrl(href)}>
|
|
||||||
<a className={activeClass}>{children}</a>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const navItems = [['/', 'Home'], ['/new', 'New Doc'], ['/settings', 'Settings']]
|
|
||||||
|
|
||||||
class Header extends Component {
|
|
||||||
state = {
|
|
||||||
open: false,
|
|
||||||
}
|
|
||||||
hideNav = () => this.setState({ open: false })
|
|
||||||
toggleNav = () =>
|
|
||||||
this.setState({
|
|
||||||
open: !this.state.open,
|
|
||||||
})
|
|
||||||
isActive = url => getUrl(this.props.router.pathname) === getUrl(url)
|
|
||||||
logout = e => {
|
|
||||||
e.preventDefault()
|
|
||||||
this.hideNav()
|
|
||||||
doLogout()
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const expandClass = this.state.open ? ' active' : ''
|
|
||||||
const { user } = this.props
|
|
||||||
return (
|
|
||||||
<nav
|
|
||||||
className={'navbar ' + css(style)}
|
|
||||||
role="navigation"
|
|
||||||
aria-label="main navigation"
|
|
||||||
>
|
|
||||||
<div className="navbar-brand">
|
|
||||||
<NavLink href="/">
|
|
||||||
<h3 onClick={this.hideNav}>MYKB</h3>
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!user.email
|
|
||||||
? null
|
|
||||||
: [
|
|
||||||
<div
|
|
||||||
className={'navbar-burger ' + expandClass}
|
|
||||||
onClick={this.toggleNav}
|
|
||||||
key="burger"
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
<div />
|
|
||||||
<div />
|
|
||||||
</div>,
|
|
||||||
<div className={'navbar-items' + expandClass} key="items">
|
|
||||||
{navItems.map(item => (
|
|
||||||
<NavLink
|
|
||||||
key={item[0]}
|
|
||||||
href={item[0]}
|
|
||||||
active={this.isActive(item[0])}
|
|
||||||
>
|
|
||||||
<p className="item" onClick={this.hideNav}>
|
|
||||||
{item[1]}
|
|
||||||
</p>
|
|
||||||
</NavLink>
|
|
||||||
))}
|
|
||||||
<a href="/logout" onClick={this.logout}>
|
|
||||||
<p className="item">Logout</p>
|
|
||||||
</a>
|
|
||||||
</div>,
|
|
||||||
]}
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default withRouter(connect(mapUser)(Header))
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { Component } from 'react'
|
|
||||||
import Router from 'next/router'
|
|
||||||
import getUrl from '../util/getUrl'
|
|
||||||
import { getKey } from '../util/keys'
|
|
||||||
import { doLogout } from '../redux/actions/userAct'
|
|
||||||
|
|
||||||
/* - keyboard shortcuts
|
|
||||||
g then h -> navigate home
|
|
||||||
g then n -> navigate to new doc
|
|
||||||
g then s -> navigate to settings
|
|
||||||
g then l -> logout
|
|
||||||
e (when on doc page) -> edit doc
|
|
||||||
/ (when on home page) -> focus search
|
|
||||||
ctrl/cmd + enter -> submit new doc (handled in CodeMirror component)
|
|
||||||
*/
|
|
||||||
const keyToUrl = {
|
|
||||||
72: '/',
|
|
||||||
78: '/new',
|
|
||||||
83: '/settings',
|
|
||||||
}
|
|
||||||
const keyToEl = {
|
|
||||||
69: { sel: '#edit', func: 'click' },
|
|
||||||
191: { sel: '.search', func: 'focus' },
|
|
||||||
}
|
|
||||||
|
|
||||||
class KeyShortcuts extends Component {
|
|
||||||
handleDown = e => {
|
|
||||||
const tag = e.target.tagName
|
|
||||||
if (tag === 'INPUT' || tag === 'TEXTAREA') return
|
|
||||||
const key = getKey(e)
|
|
||||||
if (this.prevKey === 71) {
|
|
||||||
// prev key was g
|
|
||||||
switch (key) {
|
|
||||||
case 72:
|
|
||||||
case 78:
|
|
||||||
case 83: {
|
|
||||||
const url = keyToUrl[key]
|
|
||||||
Router.push(url, getUrl(url))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 76: {
|
|
||||||
setTimeout(doLogout, 1)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (key) {
|
|
||||||
case 69:
|
|
||||||
case 191: {
|
|
||||||
const { sel, func } = keyToEl[key]
|
|
||||||
const el = document.querySelector(sel)
|
|
||||||
if (el) setTimeout(() => el[func](), 1)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
this.prevKey = key
|
|
||||||
}
|
|
||||||
componentDidMount() {
|
|
||||||
window.addEventListener('keydown', this.handleDown)
|
|
||||||
}
|
|
||||||
componentWillUnmount() {
|
|
||||||
window.removeEventListener('keydown', this.handleDown)
|
|
||||||
}
|
|
||||||
render = () => null
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KeyShortcuts
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import { doLogin } from '../redux/actions/userAct'
|
|
||||||
import Spinner from './Spinner'
|
|
||||||
import PaddedRow from './PaddedRow'
|
|
||||||
import mapUser from '../util/mapUser'
|
|
||||||
|
|
||||||
class Login extends Component {
|
|
||||||
state = {
|
|
||||||
email: '',
|
|
||||||
pass: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
updVal = e => {
|
|
||||||
const el = e.target
|
|
||||||
const val = el.value
|
|
||||||
if (el.getAttribute('type') === 'email') {
|
|
||||||
return this.setState({ email: val })
|
|
||||||
}
|
|
||||||
this.setState({ pass: val })
|
|
||||||
}
|
|
||||||
|
|
||||||
submit = e => {
|
|
||||||
const { pending } = this.props.user
|
|
||||||
let { email, pass } = this.state
|
|
||||||
email = email.trim()
|
|
||||||
pass = pass.trim()
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (pending || email.length === 0 || pass.length == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
doLogin({ email, password: pass })
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { pending, error } = this.props.user
|
|
||||||
return (
|
|
||||||
<div className="container content">
|
|
||||||
<PaddedRow amount={25} vCenter>
|
|
||||||
<h4>Please login to continue</h4>
|
|
||||||
<form noValidate>
|
|
||||||
<fieldset>
|
|
||||||
<label htmlFor="email">Email:</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
autoFocus
|
|
||||||
placeholder="John@deux.com"
|
|
||||||
onChange={this.updVal}
|
|
||||||
/>
|
|
||||||
<label htmlFor="pass">Pass:</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="pass"
|
|
||||||
name="password"
|
|
||||||
placeholder="Super secret password..."
|
|
||||||
onChange={this.updVal}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
<button
|
|
||||||
className={'float-right' + (pending ? ' disabled' : '')}
|
|
||||||
onClick={this.submit}
|
|
||||||
>
|
|
||||||
{pending ? <Spinner /> : 'Submit'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{!error ? null : <p>{error}</p>}
|
|
||||||
</form>
|
|
||||||
</PaddedRow>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default connect(mapUser)(Login)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import dynamic from 'next/dynamic'
|
|
||||||
import freezeSSR from '../util/freezeSSR'
|
|
||||||
|
|
||||||
const Markdown = dynamic(import('react-markdown'), freezeSSR('.Markdown'))
|
|
||||||
const link = props => <a {...props} target="_blank" rel="noopener noreferrer" />
|
|
||||||
const renderers = { link }
|
|
||||||
const AddRenderers = ({ className, source }) => (
|
|
||||||
<Markdown {...{ className, source, renderers }} />
|
|
||||||
)
|
|
||||||
|
|
||||||
export default AddRenderers
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import Router from 'next/router'
|
|
||||||
import dynamic from 'next/dynamic'
|
|
||||||
import getUrl from '../util/getUrl'
|
|
||||||
import getJwt from '../util/getJwt'
|
|
||||||
import Page from '../components/Page'
|
|
||||||
import Markdown from '../components/Markdown'
|
|
||||||
import updStateFromId from '../util/updStateFromId'
|
|
||||||
import { checkDir, checkName } from '../util/checkDirParts'
|
|
||||||
import '../styles/monokai'
|
|
||||||
import '../styles/codemirror'
|
|
||||||
|
|
||||||
const CodeMirrorSkel = () => (
|
|
||||||
<div className="column">
|
|
||||||
<textarea style={{ height: 'calc(300px - 1.2rem)', margin: 0 }} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
const CodeMirror = dynamic(
|
|
||||||
typeof window !== 'undefined' && import('../components/CodeMirror'),
|
|
||||||
{
|
|
||||||
loading: CodeMirrorSkel,
|
|
||||||
ssr: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const initState = {
|
|
||||||
name: '',
|
|
||||||
dir: '',
|
|
||||||
md: '## New Document!!',
|
|
||||||
editMode: false,
|
|
||||||
error: null,
|
|
||||||
pending: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
class MngDoc extends Component {
|
|
||||||
state = initState
|
|
||||||
|
|
||||||
updVal = updStateFromId.bind(this)
|
|
||||||
|
|
||||||
updMd = md => this.setState({ md })
|
|
||||||
|
|
||||||
submit = async () => {
|
|
||||||
let { name, md, dir, editMode } = this.state
|
|
||||||
let data = {
|
|
||||||
name: checkName(name),
|
|
||||||
dir: checkDir(dir),
|
|
||||||
md,
|
|
||||||
}
|
|
||||||
const doErr = error => this.setState({ pending: false, error })
|
|
||||||
const dirErr =
|
|
||||||
'can only contain A-Z, a-z, 0-9, -, or . and not start or end with .'
|
|
||||||
|
|
||||||
if (!data.name)
|
|
||||||
return doErr(
|
|
||||||
'Document name ' + (data.name === 0 ? 'can not be empty' : dirErr)
|
|
||||||
)
|
|
||||||
if (!data.dir && data.dir !== 0) {
|
|
||||||
return doErr('Directory ' + dirErr)
|
|
||||||
} else if (data.dir === 0) {
|
|
||||||
data.dir = ''
|
|
||||||
}
|
|
||||||
if (data.md.trim().length === 0) {
|
|
||||||
return doErr('Content can not be empty')
|
|
||||||
}
|
|
||||||
let url = getUrl('docs'),
|
|
||||||
method = 'POST',
|
|
||||||
headers = {
|
|
||||||
Authorization: getJwt(),
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
if (editMode) {
|
|
||||||
let numRemoved = 0
|
|
||||||
const dataKeys = Object.keys(data)
|
|
||||||
dataKeys.forEach(k => {
|
|
||||||
if (data[k] === this.props.doc[k]) {
|
|
||||||
delete data[k]
|
|
||||||
numRemoved++
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (dataKeys.length === numRemoved) return
|
|
||||||
url = getUrl('docs/' + this.props.doc.id)
|
|
||||||
method = 'PATCH'
|
|
||||||
}
|
|
||||||
this.setState({ error: null, pending: true })
|
|
||||||
const res = await fetch(url, {
|
|
||||||
headers,
|
|
||||||
method,
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
}).catch(doErr)
|
|
||||||
try {
|
|
||||||
data = await res.json()
|
|
||||||
} catch (err) {
|
|
||||||
data = { message: 'An error occurred submitting doc' }
|
|
||||||
}
|
|
||||||
if (res.ok) {
|
|
||||||
const { id } = data
|
|
||||||
return Router.push(
|
|
||||||
{
|
|
||||||
pathname: '/k',
|
|
||||||
query: { id },
|
|
||||||
},
|
|
||||||
getUrl(`k/${id}`)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
doErr(data.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
|
||||||
const { doc } = nextProps
|
|
||||||
if (doc && !prevState.didInit) {
|
|
||||||
const { name, dir, md } = doc
|
|
||||||
return { name, md, dir, editMode: true, didInit: true }
|
|
||||||
} else if (!prevState.didInit && prevState.id) {
|
|
||||||
return { ...initState, didInit: true }
|
|
||||||
} else if (!prevState.didInit) {
|
|
||||||
return { didInit: true }
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { md, dir, name, error, pending } = this.state
|
|
||||||
const rowStyle = { paddingTop: 10 }
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<div className="row fill" style={rowStyle}>
|
|
||||||
<div className="column column-50">
|
|
||||||
<Markdown className="fill Markdown" source={md} />
|
|
||||||
</div>
|
|
||||||
<div className="column column-50">
|
|
||||||
<div className="row">
|
|
||||||
<div className="column column-60">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
maxLength={250}
|
|
||||||
placeholder="New document name"
|
|
||||||
id="name"
|
|
||||||
value={name}
|
|
||||||
onChange={this.updVal}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="column">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
maxLength={1024}
|
|
||||||
placeholder="Subdirectory (optional)"
|
|
||||||
id="dir"
|
|
||||||
value={dir}
|
|
||||||
onChange={this.updVal}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<CodeMirror
|
|
||||||
value={md}
|
|
||||||
className="column WrapCodeMirror"
|
|
||||||
onChange={this.updMd}
|
|
||||||
onSubmit={this.submit}
|
|
||||||
options={{
|
|
||||||
theme: 'monokai',
|
|
||||||
mode: 'markdown',
|
|
||||||
lineWrapping: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="row" style={{ marginTop: 5 }}>
|
|
||||||
<div className="column">
|
|
||||||
<span>{error}</span>
|
|
||||||
<button
|
|
||||||
className="float-right"
|
|
||||||
style={{ marginTop: 5 }}
|
|
||||||
onClick={pending ? null : this.submit}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MngDoc
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const PaddedRow = ({ children, amount, vCenter }) => {
|
|
||||||
amount = amount || 20
|
|
||||||
const PadItem = () => <div className={'column column-' + amount + ' nomob'} />
|
|
||||||
let rowProps = { className: 'row' }
|
|
||||||
if (vCenter) rowProps = { className: 'row v-center' }
|
|
||||||
else rowProps = { ...rowProps, style: { paddingTop: amount } }
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...rowProps}>
|
|
||||||
<PadItem />
|
|
||||||
<div className="column">{children}</div>
|
|
||||||
<PadItem />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default PaddedRow
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { connect } from 'react-redux'
|
|
||||||
import Header from './Header'
|
|
||||||
import KeyShortcuts from './KeyShortcuts'
|
|
||||||
import Footer from './Footer'
|
|
||||||
import Login from './Login'
|
|
||||||
import Setup from './Setup'
|
|
||||||
import mapUser from '../util/mapUser'
|
|
||||||
|
|
||||||
const Page = ({ user, children }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Header />
|
|
||||||
<KeyShortcuts />
|
|
||||||
{(() => {
|
|
||||||
if (user.email) {
|
|
||||||
return <div className="container content">{children}</div>
|
|
||||||
}
|
|
||||||
return user.setup ? <Setup /> : <Login {...{ user }} />
|
|
||||||
})()}
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default connect(mapUser)(Page)
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import { doLogin } from '../redux/actions/userAct'
|
|
||||||
import PaddedRow from './PaddedRow'
|
|
||||||
import Spinner from './Spinner'
|
|
||||||
import getUrl from '../util/getUrl'
|
|
||||||
|
|
||||||
export default class Setup extends Component {
|
|
||||||
state = {
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
confirmPass: '',
|
|
||||||
pending: false,
|
|
||||||
error: null,
|
|
||||||
}
|
|
||||||
updVal = e => {
|
|
||||||
const el = e.target
|
|
||||||
let key = 'email'
|
|
||||||
if (el.id === 'pass') key = 'password'
|
|
||||||
else if (el.id === 'pass2') key = 'confirmPass'
|
|
||||||
const obj = {}
|
|
||||||
obj[key] = el.value
|
|
||||||
this.setState(obj)
|
|
||||||
}
|
|
||||||
submit = e => {
|
|
||||||
e.preventDefault()
|
|
||||||
let { email, password, confirmPass, pending } = this.state
|
|
||||||
if (pending) return
|
|
||||||
email = email.trim()
|
|
||||||
password = password.trim()
|
|
||||||
confirmPass = confirmPass.trim()
|
|
||||||
const hasEmpty = [email, password, confirmPass].some(
|
|
||||||
val => val.length === 0
|
|
||||||
)
|
|
||||||
|
|
||||||
if (hasEmpty) return
|
|
||||||
if (password.toLowerCase() !== confirmPass.toLowerCase()) {
|
|
||||||
return this.setState({ error: "Passwords don't match" })
|
|
||||||
}
|
|
||||||
this.setState({ pending: true, error: null })
|
|
||||||
const defaultErr = 'Could not create account'
|
|
||||||
|
|
||||||
fetch(getUrl('users'), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ email, password, admin: true }),
|
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
return doLogin({ email, password }, null, true)
|
|
||||||
}
|
|
||||||
res.json().then(({ message }) => {
|
|
||||||
const error = message || defaultErr
|
|
||||||
this.setState({ pending: false, error })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
const error = err.message || defaultErr
|
|
||||||
this.setState({ pending: false, error })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
const { pending, error } = this.state
|
|
||||||
return (
|
|
||||||
<div className="container content">
|
|
||||||
<PaddedRow amount={25} vCenter>
|
|
||||||
<div className="column">
|
|
||||||
<h3>Setup account</h3>
|
|
||||||
<form noValidate>
|
|
||||||
<fieldset>
|
|
||||||
<label htmlFor="email">Email:</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
autoFocus
|
|
||||||
id="email"
|
|
||||||
placeholder={"Your email (does't have to be actual email)"}
|
|
||||||
onChange={this.updVal}
|
|
||||||
/>
|
|
||||||
<label htmlFor="pass">Password:</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="pass"
|
|
||||||
maxLength={512}
|
|
||||||
placeholder="A super secret password"
|
|
||||||
onChange={this.updVal}
|
|
||||||
/>
|
|
||||||
<label htmlFor="pass2">Confirm Password:</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="pass2"
|
|
||||||
maxLength={512}
|
|
||||||
placeholder="Confirm your super secret password"
|
|
||||||
onChange={this.updVal}
|
|
||||||
/>
|
|
||||||
<button className="float-right" onClick={this.submit}>
|
|
||||||
{pending ? <Spinner /> : 'Submit'}
|
|
||||||
</button>
|
|
||||||
{!error ? null : <p className="danger">{error}</p>}
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</PaddedRow>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
const Spinner = props => <div className="spinner" {...props} />
|
|
||||||
export default Spinner
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
// A hook that logs service method before, after and error
|
|
||||||
// See https://github.com/winstonjs/winston for documentation
|
|
||||||
// about the logger.
|
|
||||||
const logger = require('winston')
|
|
||||||
|
|
||||||
// To see more detailed messages, uncomment the following line
|
|
||||||
// logger.level = 'debug';
|
|
||||||
|
|
||||||
module.exports = function() {
|
|
||||||
return context => {
|
|
||||||
// This debugs the service call and a stringified version of the hook context
|
|
||||||
// You can customize the mssage (and logger) to your needs
|
|
||||||
// logger.debug(
|
|
||||||
// `${context.type} app.service('${context.path}').${context.method}()`
|
|
||||||
// )
|
|
||||||
// if (typeof context.toJSON === 'function') {
|
|
||||||
// logger.debug('Hook Context', JSON.stringify(context, null, ' '))
|
|
||||||
// }
|
|
||||||
// if (context.error) {
|
|
||||||
// logger.error(context.error)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
65
src/index.js
65
src/index.js
@@ -1,11 +1,60 @@
|
|||||||
const logger = require('winston')
|
const http = require('http')
|
||||||
const app = require('./app')
|
const path = require('path')
|
||||||
const port = app.get('port')
|
const Next = require('next')
|
||||||
|
const chokidar = require('chokidar')
|
||||||
|
const isDev = process.env.NODE_ENV !== 'production'
|
||||||
|
const next = Next({ dev: isDev, quiet: true })
|
||||||
|
|
||||||
app.run(port).then(() => {
|
let server = null
|
||||||
logger.info('MYKB listening at http://%s:%d', app.get('host'), port)
|
let creatingServer = false
|
||||||
|
|
||||||
|
// prepare next
|
||||||
|
next.prepare()
|
||||||
|
global.next = next
|
||||||
|
|
||||||
|
async function createServer() {
|
||||||
|
if (creatingServer) return
|
||||||
|
creatingServer = true
|
||||||
|
const { port } = await require('./server/util/config')
|
||||||
|
|
||||||
|
if (server) {
|
||||||
|
console.log('Restarting server...')
|
||||||
|
await new Promise(resolve => {
|
||||||
|
server.destroy(() => resolve())
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, p) =>
|
try {
|
||||||
logger.error('Unhandled Rejection at: Promise ', p, reason)
|
server = http.createServer(require('./server/server'))
|
||||||
)
|
isDev && require('server-destroy')(server)
|
||||||
|
server.listen(port)
|
||||||
|
server.once('listening', () => {
|
||||||
|
creatingServer = false
|
||||||
|
console.log(`Listening at http://127.0.0.1:${port}`)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
creatingServer = false
|
||||||
|
console.log('waiting for change to restart...')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
// watch for server changes and hot reload server
|
||||||
|
// without having to reload next.js
|
||||||
|
const configPath = path.resolve('./config')
|
||||||
|
const utilPath = path.resolve('./src/util')
|
||||||
|
const serverPath = path.resolve('./src/server')
|
||||||
|
const watcher = chokidar.watch([serverPath, configPath, utilPath], {})
|
||||||
|
|
||||||
|
watcher.on('change', path => {
|
||||||
|
Object.keys(require.cache).forEach(key => {
|
||||||
|
if (key.indexOf(serverPath) > -1 || key.indexOf(utilPath) > -1) {
|
||||||
|
delete require.cache[key]
|
||||||
|
delete module.constructor._pathCache[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
createServer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
createServer()
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
module.exports = function(app) {
|
|
||||||
// Add your custom middleware here. Remember, that
|
|
||||||
// in Express the order matters
|
|
||||||
|
|
||||||
// add req.ip to feathers
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
req.feathers.ip = req.ip
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
const NeDB = require('nedb')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
module.exports = function(app) {
|
|
||||||
const dbPath = app.get('nedb')
|
|
||||||
const Model = new NeDB({
|
|
||||||
filename: path.join(dbPath, 'users.db'),
|
|
||||||
autoload: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
Model.ensureIndex({ fieldName: 'email', unique: true })
|
|
||||||
|
|
||||||
return Model
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import fetch from 'isomorphic-unfetch'
|
|
||||||
import store from '../store'
|
|
||||||
import getUrl from '../../util/getUrl'
|
|
||||||
|
|
||||||
// define action types
|
|
||||||
export const SET_USER = 'SET_USER'
|
|
||||||
export const LOGIN_PENDING = 'LOGIN_PENDING'
|
|
||||||
export const LOGIN_FAILED = 'LOGIN_FAILED'
|
|
||||||
export const LOGOUT = 'LOGOUT'
|
|
||||||
|
|
||||||
export const setUser = user => {
|
|
||||||
store.dispatch({
|
|
||||||
type: SET_USER,
|
|
||||||
data: user,
|
|
||||||
})
|
|
||||||
} // setUser
|
|
||||||
|
|
||||||
export const doLogout = () => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.localStorage.removeItem('jwt')
|
|
||||||
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;'
|
|
||||||
}
|
|
||||||
store.dispatch({ type: LOGOUT })
|
|
||||||
} // doLogout
|
|
||||||
|
|
||||||
export async function doLogin(creds, jwt, noPend) {
|
|
||||||
!noPend && store.dispatch({ type: LOGIN_PENDING })
|
|
||||||
const authReqOpts = { method: 'POST', credentials: 'include' }
|
|
||||||
const authReqHead = {
|
|
||||||
headers: jwt
|
|
||||||
? { Authorization: jwt }
|
|
||||||
: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const authReqBody = jwt
|
|
||||||
? null
|
|
||||||
: {
|
|
||||||
body: JSON.stringify({ ...creds, strategy: 'local' }),
|
|
||||||
}
|
|
||||||
const authReq = new Request(getUrl('auth'), {
|
|
||||||
...authReqOpts,
|
|
||||||
...authReqHead,
|
|
||||||
...authReqBody,
|
|
||||||
})
|
|
||||||
const authRes = await fetch(authReq).catch(err => {
|
|
||||||
store.dispatch({ type: LOGIN_FAILED, data: err.message })
|
|
||||||
})
|
|
||||||
if (!authRes.ok) {
|
|
||||||
let error
|
|
||||||
try {
|
|
||||||
error = await authRes.json()
|
|
||||||
error = error.message
|
|
||||||
} catch (err) {
|
|
||||||
error =
|
|
||||||
authRes.status === 429
|
|
||||||
? 'Max login attempts reached'
|
|
||||||
: 'An error occurred during login'
|
|
||||||
}
|
|
||||||
return store.dispatch({
|
|
||||||
type: LOGIN_FAILED,
|
|
||||||
data: error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const { accessToken } = await authRes.json()
|
|
||||||
const payload = accessToken.split('.')[1]
|
|
||||||
const { userId } = JSON.parse(atob(payload))
|
|
||||||
const userReq = new Request(getUrl(`/users/${userId}`), {
|
|
||||||
headers: {
|
|
||||||
Authorization: accessToken,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const userRes = await fetch(userReq)
|
|
||||||
if (!userRes.ok) {
|
|
||||||
return store.dispatch({
|
|
||||||
type: LOGIN_FAILED,
|
|
||||||
data: 'failed to get user',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
window.localStorage.setItem('jwt', accessToken)
|
|
||||||
setUser(await userRes.json())
|
|
||||||
} // doLogin
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import {
|
|
||||||
SET_USER,
|
|
||||||
LOGIN_PENDING,
|
|
||||||
LOGIN_FAILED,
|
|
||||||
LOGOUT,
|
|
||||||
} from '../actions/userAct'
|
|
||||||
|
|
||||||
const initState = {
|
|
||||||
setup: false,
|
|
||||||
_id: null,
|
|
||||||
email: null,
|
|
||||||
admin: null,
|
|
||||||
pending: false,
|
|
||||||
error: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
function user(state = initState, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case SET_USER: {
|
|
||||||
return {
|
|
||||||
...initState,
|
|
||||||
...action.data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case LOGIN_PENDING: {
|
|
||||||
return {
|
|
||||||
...initState,
|
|
||||||
pending: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case LOGIN_FAILED: {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
pending: false,
|
|
||||||
error: action.data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case LOGOUT: {
|
|
||||||
return initState
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default user
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { applyMiddleware, combineReducers, createStore } from 'redux'
|
|
||||||
|
|
||||||
import user from './reducers/userRed'
|
|
||||||
|
|
||||||
let middleware
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
const logger = require('redux-logger').default
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
middleware = applyMiddleware(logger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reducers = combineReducers({
|
|
||||||
user,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default (middleware
|
|
||||||
? createStore(reducers, middleware)
|
|
||||||
: createStore(reducers))
|
|
||||||
90
src/server/middleware.js
Normal file
90
src/server/middleware.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const DB = require('./util/db')
|
||||||
|
const bcrypt = require('bcrypt')
|
||||||
|
const helmet = require('helmet')
|
||||||
|
const express = require('express')
|
||||||
|
const passport = require('passport')
|
||||||
|
const bodyParser = require('body-parser')
|
||||||
|
const compression = require('compression')
|
||||||
|
const cookieParser = require('cookie-parser')
|
||||||
|
const { Strategy } = require('passport-local')
|
||||||
|
const jwtExtract = require('./util/jwtExtract')
|
||||||
|
const { Strategy: JwtStrategy } = require('passport-jwt')
|
||||||
|
|
||||||
|
const publicDir = path.join(__dirname, '../../public')
|
||||||
|
const usersDb = path.join(__dirname, '../../config/users.json')
|
||||||
|
const users = new DB(usersDb)
|
||||||
|
|
||||||
|
passport.use(
|
||||||
|
new Strategy(function(username, password, cb) {
|
||||||
|
let user = null
|
||||||
|
const found = Object.keys(users.data).some(id => {
|
||||||
|
const curUser = users.data[id]
|
||||||
|
if (curUser.username === username) {
|
||||||
|
user = curUser
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!found) return cb(null, false)
|
||||||
|
|
||||||
|
bcrypt.compare(password, user.password).then(match => {
|
||||||
|
cb(null, match && user)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
passport.serializeUser(function(user, cb) {
|
||||||
|
cb(null, user.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
passport.deserializeUser(function(id, cb) {
|
||||||
|
const { password, ...user } = users.data[id] || {}
|
||||||
|
cb(null, user.id && user)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = function middleware(app) {
|
||||||
|
const jwtOpts = {
|
||||||
|
...app.config.jwt,
|
||||||
|
jwtFromRequest: jwtExtract,
|
||||||
|
secretOrKey: app.config.secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
passport.use(
|
||||||
|
new JwtStrategy(jwtOpts, function(jwtPayload, cb) {
|
||||||
|
const id = jwtPayload.user.id
|
||||||
|
const { password, ...user } = users.data[id]
|
||||||
|
return cb(null, user)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// serve public path
|
||||||
|
app.use('/', express.static(publicDir))
|
||||||
|
|
||||||
|
// add gzip support
|
||||||
|
app.use(compression())
|
||||||
|
|
||||||
|
// add helpful headers
|
||||||
|
app.use(helmet({ hidePoweredBy: { setTo: 'hamsters' } }))
|
||||||
|
|
||||||
|
// set up passport.js
|
||||||
|
app.use(cookieParser())
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }))
|
||||||
|
app.use(passport.initialize())
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
passport.authenticate('jwt', function(err, user, info) {
|
||||||
|
if (user) req.user = user
|
||||||
|
next()
|
||||||
|
})(req, res, next)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (!Object.keys(users.data).length) {
|
||||||
|
global.publicConfig.doSetup = true
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.users = users
|
||||||
|
app.passport = passport
|
||||||
|
}
|
||||||
33
src/server/routes/auth.js
Normal file
33
src/server/routes/auth.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const rateLimit = require('express-rate-limit')
|
||||||
|
const handleLogin = require('../util/handleLogin')
|
||||||
|
const { userError } = require('../util/responses')
|
||||||
|
/**
|
||||||
|
* sets up auth endpoint
|
||||||
|
*/
|
||||||
|
module.exports = function auth(app) {
|
||||||
|
const { passport } = app
|
||||||
|
|
||||||
|
if (!app.config.dev) {
|
||||||
|
const authLimiter = rateLimit({
|
||||||
|
windowMs: 2 * 60 * 1000, // 2 minutes
|
||||||
|
max: 10, // limit each IP to 10 requests per windowMs
|
||||||
|
})
|
||||||
|
app.use('/auth', authLimiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post('/auth', (req, res) => {
|
||||||
|
// allow checking JWT with Authorization header
|
||||||
|
if (req.get('authorization')) {
|
||||||
|
return res
|
||||||
|
.status(req.user ? 200 : 400)
|
||||||
|
.json({ status: req.user ? 'ok' : 'error' })
|
||||||
|
}
|
||||||
|
|
||||||
|
passport.authenticate('local', {}, (err, user, info) => {
|
||||||
|
if (err || !user) {
|
||||||
|
return userError(res, 'Invalid login')
|
||||||
|
}
|
||||||
|
handleLogin(req, res, user, app)
|
||||||
|
})(req, res)
|
||||||
|
})
|
||||||
|
}
|
||||||
168
src/server/routes/docs.js
Normal file
168
src/server/routes/docs.js
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
const KB = require('../util/kb')
|
||||||
|
const Git = require('simple-git/promise')
|
||||||
|
const isOkDoc = require('../util/isOkDoc')
|
||||||
|
const tryRmdir = require('../util/tryRmdir')
|
||||||
|
const requireUser = require('../util/requireUser')
|
||||||
|
const { aOk, notFound, serverError, userError } = require('../util/responses')
|
||||||
|
const {
|
||||||
|
sortDocs,
|
||||||
|
searchDocs,
|
||||||
|
limitDocs,
|
||||||
|
buildSortBy,
|
||||||
|
} = require('../../util/kbHelpers')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sets up docs endpoint
|
||||||
|
*/
|
||||||
|
module.exports = function docs(app) {
|
||||||
|
let git
|
||||||
|
const {
|
||||||
|
useGit,
|
||||||
|
docsDir,
|
||||||
|
cacheSize,
|
||||||
|
maxDocSize,
|
||||||
|
maxDocsLimit,
|
||||||
|
defDocsLimit,
|
||||||
|
} = app.config
|
||||||
|
|
||||||
|
const kb = new KB({
|
||||||
|
cacheSize,
|
||||||
|
maxDocSize,
|
||||||
|
kbPath: docsDir,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (useGit) {
|
||||||
|
git = Git(docsDir)
|
||||||
|
kb.loaded.then(() => {
|
||||||
|
const numDocs = Object.keys(kb.docs).length
|
||||||
|
|
||||||
|
// check if git needs to be initialized in docs dir
|
||||||
|
fs.stat(path.join(docsDir, '.git'), err => {
|
||||||
|
if (err && err.code === 'ENOENT') {
|
||||||
|
git.init().then(() => {
|
||||||
|
git.addConfig('user.name', 'mykb')
|
||||||
|
git.addConfig('user.email', 'mykb@localhost')
|
||||||
|
if (numDocs === 0) return
|
||||||
|
git.add('./*').then(() => git.commit('initial commit'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure user is logged in
|
||||||
|
app.use('/docs*', requireUser)
|
||||||
|
|
||||||
|
// handle getting docs
|
||||||
|
app.get('/docs', (req, res) => {
|
||||||
|
let docs
|
||||||
|
const { query } = req
|
||||||
|
|
||||||
|
if (query.id) {
|
||||||
|
const doc = kb.docs[query.id]
|
||||||
|
if (!doc) return notFound(res)
|
||||||
|
return aOk(res, doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.search) {
|
||||||
|
docs = searchDocs(kb.docs, query.search)
|
||||||
|
}
|
||||||
|
const sortBy = buildSortBy(query)
|
||||||
|
docs = sortDocs(kb.docs, sortBy, docs && docs.map(doc => doc.id))
|
||||||
|
const { total, hasMore, results } = limitDocs(
|
||||||
|
docs,
|
||||||
|
query,
|
||||||
|
defDocsLimit,
|
||||||
|
maxDocsLimit
|
||||||
|
)
|
||||||
|
|
||||||
|
aOk(res, { total, results, hasMore })
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle new doc
|
||||||
|
app.post('/docs', async (req, res) => {
|
||||||
|
const { name, dir, md } = req.body
|
||||||
|
if (!isOkDoc({ name, dir, md }, kb, res, true)) return
|
||||||
|
try {
|
||||||
|
const docPath = path.join(docsDir, dir || '', name)
|
||||||
|
await fs.outputFile(docPath, md)
|
||||||
|
useGit && (await git.add(docPath))
|
||||||
|
useGit && (await git.commit(`added doc ${docPath.split(docsDir)[1]}`))
|
||||||
|
} catch (err) {
|
||||||
|
return serverError(res, err)
|
||||||
|
}
|
||||||
|
const added = new Date()
|
||||||
|
const doc = kb.setDoc(path.join(dir || '', name), added, md, added)
|
||||||
|
aOk(res, doc)
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle update
|
||||||
|
app.patch('/docs', async (req, res) => {
|
||||||
|
const { query, body } = req
|
||||||
|
const doc = kb.docs[query.id]
|
||||||
|
const { name, dir, md } = body
|
||||||
|
if (!doc) return notFound(res)
|
||||||
|
if (!isOkDoc({ name: name || doc.name, dir, md }, kb, res)) return
|
||||||
|
let newDir = typeof dir === 'string' ? dir : doc.dir
|
||||||
|
const oldDir = path.join(docsDir, doc.dir)
|
||||||
|
const oldPath = path.join(oldDir, doc.name)
|
||||||
|
const docPath = path.join(docsDir, newDir, name || doc.name)
|
||||||
|
const oldRelPath = oldPath.split(docsDir + '/')[1]
|
||||||
|
const curRelPath = docPath.split(docsDir + '/')[1]
|
||||||
|
const isNewPath = oldPath !== docPath
|
||||||
|
|
||||||
|
if (isNewPath && kb.docs[curRelPath]) {
|
||||||
|
return userError(res, 'item already exists at new path')
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (md) {
|
||||||
|
await fs.outputFile(docPath, md)
|
||||||
|
isNewPath && (await fs.remove(oldPath))
|
||||||
|
} else if (isNewPath) {
|
||||||
|
await fs.move(oldPath, docPath)
|
||||||
|
}
|
||||||
|
await tryRmdir(docsDir, oldRelPath)
|
||||||
|
|
||||||
|
if (useGit && isNewPath) {
|
||||||
|
await git.rm(oldPath)
|
||||||
|
await git.add(docPath)
|
||||||
|
await git.commit(`renamed doc ${oldRelPath} to ${curRelPath}`)
|
||||||
|
} else if (useGit) {
|
||||||
|
await git.add(docPath)
|
||||||
|
await git.commit(`updated doc ${curRelPath}`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return serverError(res, err)
|
||||||
|
}
|
||||||
|
const updated = new Date()
|
||||||
|
const updatedDoc = kb.setDoc(curRelPath, doc.created, md || doc.md, updated)
|
||||||
|
aOk(res, updatedDoc)
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle delete doc
|
||||||
|
app.delete('/docs', async (req, res) => {
|
||||||
|
const { query } = req
|
||||||
|
const doc = { ...kb.docs[query.id] }
|
||||||
|
|
||||||
|
if (!doc) {
|
||||||
|
return notFound(res)
|
||||||
|
}
|
||||||
|
const toRemove = path.join(docsDir, doc.id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.remove(toRemove)
|
||||||
|
// try removing directory to clean up if was only file in dir
|
||||||
|
if (doc.dir) {
|
||||||
|
tryRmdir(docsDir, doc.dir)
|
||||||
|
}
|
||||||
|
useGit && (await git.rm(doc.id))
|
||||||
|
useGit && (await git.commit(`removed doc ${doc.id}`))
|
||||||
|
} catch (err) {
|
||||||
|
return serverError(res, err)
|
||||||
|
}
|
||||||
|
delete kb.docs[query.id]
|
||||||
|
aOk(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
11
src/server/routes/logout.js
Normal file
11
src/server/routes/logout.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const addBase = require('../../util/addBase')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sets up logout endpoint
|
||||||
|
*/
|
||||||
|
module.exports = function logout(app) {
|
||||||
|
app.get('/logout', (req, res) => {
|
||||||
|
res.clearCookie('jwt')
|
||||||
|
res.redirect(addBase('/'))
|
||||||
|
})
|
||||||
|
}
|
||||||
38
src/server/routes/register.js
Normal file
38
src/server/routes/register.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const bcrypt = require('bcrypt')
|
||||||
|
const uuid = require('uuid/v4')
|
||||||
|
const handleLogin = require('../util/handleLogin')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sets up register endpoint
|
||||||
|
*/
|
||||||
|
module.exports = function register(app) {
|
||||||
|
const { users } = app
|
||||||
|
|
||||||
|
app.post('/register', (req, res) => {
|
||||||
|
if (!global.publicConfig.doSetup) {
|
||||||
|
return res.status(401).json({ message: 'already set up' })
|
||||||
|
}
|
||||||
|
const { username, password } = req.body
|
||||||
|
const taken = Object.keys(users.data).some(id => {
|
||||||
|
const user = users.data[id]
|
||||||
|
return user.username === username
|
||||||
|
})
|
||||||
|
if (taken) {
|
||||||
|
return res.send('username taken')
|
||||||
|
}
|
||||||
|
bcrypt.hash(password, 10).then(hash => {
|
||||||
|
const id = uuid()
|
||||||
|
const user = {
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
password: hash,
|
||||||
|
}
|
||||||
|
users.setData({
|
||||||
|
...users.data,
|
||||||
|
[id]: user,
|
||||||
|
})
|
||||||
|
delete global.publicConfig.doSetup
|
||||||
|
handleLogin(req, res, user, app)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
44
src/server/routes/user.js
Normal file
44
src/server/routes/user.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const bcrypt = require('bcrypt')
|
||||||
|
const rateLimit = require('express-rate-limit')
|
||||||
|
const { aOk, userError, serverError } = require('../util/responses')
|
||||||
|
/**
|
||||||
|
* allows updating user
|
||||||
|
*/
|
||||||
|
module.exports = function auth(app) {
|
||||||
|
const { passport, users } = app
|
||||||
|
|
||||||
|
if (!app.config.dev) {
|
||||||
|
const authLimiter = rateLimit({
|
||||||
|
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||||
|
max: 10, // limit each IP to 10 requests per windowMs
|
||||||
|
})
|
||||||
|
app.use('/user', authLimiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.patch('/user', (req, res) => {
|
||||||
|
passport.authenticate('local', {}, (err, user, info) => {
|
||||||
|
if (err || !user) {
|
||||||
|
return userError(res, 'Invalid login')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = user
|
||||||
|
let { newPassword } = req.body
|
||||||
|
if (!newPassword) {
|
||||||
|
return userError(res, 'newPassword is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
newPassword = bcrypt.hash(newPassword, 10).then(hash => {
|
||||||
|
users
|
||||||
|
.setData({
|
||||||
|
...users.data,
|
||||||
|
[id]: {
|
||||||
|
...users.data[id],
|
||||||
|
password: hash,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => aOk(res))
|
||||||
|
.catch(err => serverError(res, err))
|
||||||
|
})
|
||||||
|
})(req, res)
|
||||||
|
})
|
||||||
|
}
|
||||||
60
src/server/server.js
Normal file
60
src/server/server.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const express = require('express')
|
||||||
|
const config = require('./util/config')
|
||||||
|
const middleware = require('./middleware')
|
||||||
|
const trustIPs = require('./util/trustIPs')
|
||||||
|
const app = express()
|
||||||
|
const router = express.Router()
|
||||||
|
const next = global.next
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
router.config = await config
|
||||||
|
const {
|
||||||
|
dev,
|
||||||
|
ssr,
|
||||||
|
port,
|
||||||
|
date,
|
||||||
|
basePath,
|
||||||
|
searchDelay,
|
||||||
|
maxDocsLimit,
|
||||||
|
defDocsLimit,
|
||||||
|
trustCloudflare,
|
||||||
|
} = router.config
|
||||||
|
|
||||||
|
// resolve paths to absolute paths
|
||||||
|
router.config.docsDir = path.resolve(router.config.docsDir)
|
||||||
|
|
||||||
|
// config made public to the client
|
||||||
|
global.publicConfig = {
|
||||||
|
dev,
|
||||||
|
ssr,
|
||||||
|
port,
|
||||||
|
date,
|
||||||
|
basePath,
|
||||||
|
searchDelay,
|
||||||
|
maxDocsLimit,
|
||||||
|
defDocsLimit,
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up proxy trusting
|
||||||
|
trustIPs(app, trustCloudflare)
|
||||||
|
|
||||||
|
// set next to use basePath
|
||||||
|
next.setAssetPrefix(basePath)
|
||||||
|
|
||||||
|
// apply middleware
|
||||||
|
middleware(router)
|
||||||
|
|
||||||
|
const routes = ['auth', 'logout', 'register', 'docs', 'user']
|
||||||
|
// set up routes
|
||||||
|
routes.forEach(route => require('./routes/' + route)(router))
|
||||||
|
|
||||||
|
// set up next handler (must come last)
|
||||||
|
router.use(next.getRequestHandler())
|
||||||
|
|
||||||
|
// prefix all routes with baseRoute
|
||||||
|
app.use(basePath, router)
|
||||||
|
}
|
||||||
|
main()
|
||||||
|
|
||||||
|
module.exports = app
|
||||||
31
src/server/util/config.js
Normal file
31
src/server/util/config.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const DB = require('./db')
|
||||||
|
const path = require('path')
|
||||||
|
const isDev = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
|
const configPath = file => path.join(__dirname, '../../../config', file)
|
||||||
|
const defaultPath = configPath('default.json')
|
||||||
|
const productionPath = configPath('production.json')
|
||||||
|
|
||||||
|
const defaultConfig = new DB(defaultPath)
|
||||||
|
const productionConfig = isDev
|
||||||
|
? { loading: Promise.resolve(), data: {} }
|
||||||
|
: new DB(productionPath)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise} - resolves with config when loaded
|
||||||
|
*/
|
||||||
|
function config() {
|
||||||
|
return defaultConfig.loading
|
||||||
|
.then(() => productionConfig.loading)
|
||||||
|
.then(() => {
|
||||||
|
return {
|
||||||
|
...defaultConfig.data,
|
||||||
|
...productionConfig.data,
|
||||||
|
ssr: true,
|
||||||
|
dev: isDev,
|
||||||
|
setData: (isDev ? defaultConfig : productionConfig).setData,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config()
|
||||||
24
src/server/util/db.js
Normal file
24
src/server/util/db.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const fs = require('fs-extra')
|
||||||
|
|
||||||
|
// read and write to a json file
|
||||||
|
module.exports = function DB(file) {
|
||||||
|
this.data = {}
|
||||||
|
|
||||||
|
this.setData = function(data) {
|
||||||
|
this.data = data
|
||||||
|
return fs.writeFile(file, JSON.stringify(data, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getData = function() {
|
||||||
|
return this.data
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = fs
|
||||||
|
.readFile(file)
|
||||||
|
.then(buf => {
|
||||||
|
this.data = JSON.parse(buf.toString())
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
29
src/server/util/handleLogin.js
Normal file
29
src/server/util/handleLogin.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const jwt = require('jsonwebtoken')
|
||||||
|
const addBase = require('../../util/addBase')
|
||||||
|
|
||||||
|
module.exports = function(req, res, user, app) {
|
||||||
|
req.login(user, {}, err => {
|
||||||
|
if (err) {
|
||||||
|
return res.send(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const curUser = Object.keys(user).reduce((obj, key) => {
|
||||||
|
if (key !== 'password') obj[key] = user[key]
|
||||||
|
return obj
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const token = jwt.sign({ user: curUser }, app.config.secret, app.config.jwt)
|
||||||
|
|
||||||
|
res.cookie('jwt', token, {
|
||||||
|
expires: new Date(Date.now() + 3600 * 24 * 7 * 1000),
|
||||||
|
httpOnly: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// redirect if using form submission instead of XHR
|
||||||
|
if (req.body.form) {
|
||||||
|
return res.redirect(addBase('/'))
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ accessToken: token })
|
||||||
|
})
|
||||||
|
}
|
||||||
34
src/server/util/isOkDoc.js
Normal file
34
src/server/util/isOkDoc.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const { userError } = require('./responses')
|
||||||
|
const isOkDirPart = require('../../util/isOkDirPart')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if doc request is ok
|
||||||
|
* @param { Object } doc - doc object with name, dir, md
|
||||||
|
* @param { Object } kb - the current kb instance
|
||||||
|
* @param { Object } res - the express.Response object
|
||||||
|
* @param { boolean } requireAll - whether to require all fields on doc
|
||||||
|
*/
|
||||||
|
module.exports = function isOkDoc({ name, dir, md }, kb, res, requireAll) {
|
||||||
|
let docPath = name
|
||||||
|
|
||||||
|
if (!md || typeof md !== 'string' || md.length === 0) {
|
||||||
|
if (requireAll) return userError(res, 'md can not be empty')
|
||||||
|
}
|
||||||
|
if (name && name.slice(-3) !== '.md') {
|
||||||
|
return userError(res, 'doc name must end in .md')
|
||||||
|
} else if (name && !isOkDirPart(name)) {
|
||||||
|
return userError(res, 'name contains an invalid character')
|
||||||
|
}
|
||||||
|
if (dir && typeof dir === 'string') {
|
||||||
|
if (dir.split('/').some(dirPart => !isOkDirPart(dirPart))) {
|
||||||
|
return userError(res, 'dir contains an invalid character')
|
||||||
|
}
|
||||||
|
docPath = path.join(dir, name)
|
||||||
|
}
|
||||||
|
if (requireAll && kb.docs[docPath]) {
|
||||||
|
return userError(res, 'item already exists')
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
19
src/server/util/jwtExtract.js
Normal file
19
src/server/util/jwtExtract.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const { ExtractJwt } = require('passport-jwt')
|
||||||
|
const bearerExtract = ExtractJwt.fromAuthHeaderAsBearerToken()
|
||||||
|
const ssrRoutes = {
|
||||||
|
'/': 1,
|
||||||
|
'/doc': 1,
|
||||||
|
'/new': 1,
|
||||||
|
'/edit': 1,
|
||||||
|
'/settings': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function jwtExtract(req) {
|
||||||
|
let token = null
|
||||||
|
if (ssrRoutes[req.path]) {
|
||||||
|
token = req.cookies.jwt
|
||||||
|
} else {
|
||||||
|
token = bearerExtract(req)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
94
src/server/util/kb.js
Normal file
94
src/server/util/kb.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
const chokidar = require('chokidar')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loads markdown files from docs directory
|
||||||
|
* and watches for changes
|
||||||
|
* @param { Object } options - the options object
|
||||||
|
*/
|
||||||
|
module.exports = function KB({
|
||||||
|
kbPath = '',
|
||||||
|
cacheSize = 10 << 20, // 10 MB
|
||||||
|
maxDocSize = 100 << 10, // 100 KB
|
||||||
|
}) {
|
||||||
|
this.docs = {}
|
||||||
|
this.cachedSize = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update doc entry (does not update doc file)
|
||||||
|
* @param { String } relPath - relative path of doc to docsDir
|
||||||
|
* @param { Date } created - date string of when doc was created
|
||||||
|
* @param { String } md - the markdown string
|
||||||
|
* @param { Date } updated - date string of when doc was last updated
|
||||||
|
* @returns { Object } - the new doc entry
|
||||||
|
*/
|
||||||
|
this.setDoc = (relPath, created, md, updated) => {
|
||||||
|
const id = relPath
|
||||||
|
let dir = relPath.split('/')
|
||||||
|
const name = dir.pop()
|
||||||
|
dir = dir.join('/')
|
||||||
|
const doc = {
|
||||||
|
name,
|
||||||
|
dir,
|
||||||
|
id,
|
||||||
|
md,
|
||||||
|
created,
|
||||||
|
updated,
|
||||||
|
}
|
||||||
|
this.docs[id] = doc
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { String } path - the absolute path to doc
|
||||||
|
* @returns { String } - relative path from docs directory
|
||||||
|
*/
|
||||||
|
this.getRelPath = path => {
|
||||||
|
path = path.split(kbPath).pop()
|
||||||
|
if (path.substr(0, 1) === '/') path = path.substr(1)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle doc add or change
|
||||||
|
* @param { String } path - absolute path to doc
|
||||||
|
* @param { Object|undefined } stats - the fs.stats object for the doc
|
||||||
|
*/
|
||||||
|
this.handleDoc = async (path, stats) => {
|
||||||
|
const relPath = this.getRelPath(path)
|
||||||
|
|
||||||
|
if (!stats) stats = await fs.stat(path)
|
||||||
|
else {
|
||||||
|
// stats were set so it's a change event
|
||||||
|
const changedDoc = this.docs[relPath]
|
||||||
|
if (changedDoc && changedDoc.md) this.cached -= changedDoc.md.length
|
||||||
|
}
|
||||||
|
const { birthtime, size, mtime } = stats
|
||||||
|
let md = null
|
||||||
|
if (size < maxDocSize && this.cachedSize + size < cacheSize) {
|
||||||
|
md = await fs.readFile(path)
|
||||||
|
md = md.toString()
|
||||||
|
this.cached += md.length
|
||||||
|
}
|
||||||
|
this.setDoc(relPath, birthtime, md, mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up watching of docs dir and populate initial data
|
||||||
|
this.watcher = chokidar.watch(path.join(kbPath, '/**/*.md'))
|
||||||
|
|
||||||
|
this.loaded = new Promise(resolve => {
|
||||||
|
this.watcher.on('ready', resolve)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.watcher.on('add', this.handleDoc)
|
||||||
|
this.watcher.on('change', this.handleDoc)
|
||||||
|
this.watcher.on('unlink', path => {
|
||||||
|
// doc removed
|
||||||
|
const id = this.getRelPath(path)
|
||||||
|
if (this.docs[id] && this.docs[id].md) {
|
||||||
|
this.cached -= this.docs[id].md.length
|
||||||
|
}
|
||||||
|
delete this.docs[id]
|
||||||
|
})
|
||||||
|
}
|
||||||
17
src/server/util/requireUser.js
Normal file
17
src/server/util/requireUser.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* returns forbidden status when user not present
|
||||||
|
* @param {Object} - express.Request
|
||||||
|
* @param {Object} - express.Response
|
||||||
|
* @returns {boolean} - whether if had user or not
|
||||||
|
*/
|
||||||
|
module.exports = function requireUser(req, res, next) {
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(403).json({
|
||||||
|
status: 'error',
|
||||||
|
message: 'You do not have permission to access this',
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (next) next()
|
||||||
|
return true
|
||||||
|
}
|
||||||
30
src/server/util/responses.js
Normal file
30
src/server/util/responses.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const isDev = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
|
// standard JSON responses
|
||||||
|
module.exports = {
|
||||||
|
aOk: (res, data) => {
|
||||||
|
res.status(200).json({ status: 'ok', ...data })
|
||||||
|
},
|
||||||
|
|
||||||
|
notFound: res => {
|
||||||
|
res.status(404).json({
|
||||||
|
status: 'error',
|
||||||
|
message: 'item not found',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
serverError: (res, err) => {
|
||||||
|
isDev && console.log(new Error(err).stack)
|
||||||
|
res.status(500).json({
|
||||||
|
status: 'error',
|
||||||
|
message: (isDev && err && err.message) || 'server encountered error',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
userError: (res, message) => {
|
||||||
|
res.status(400).json({
|
||||||
|
status: 'error',
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fetch = require('isomorphic-unfetch')
|
const fetch = require('isomorphic-unfetch')
|
||||||
const ips = require('../config/trustIPs.json')
|
const ips = require('../../../config/trustIPs.json')
|
||||||
ips.push('loopback')
|
ips.push('loopback')
|
||||||
|
|
||||||
const cfv4 = 'https://www.cloudflare.com/ips-v4'
|
const cfv4 = 'https://www.cloudflare.com/ips-v4'
|
||||||
const cfv6 = 'https://www.cloudflare.com/ips-v6'
|
const cfv6 = 'https://www.cloudflare.com/ips-v6'
|
||||||
const cfConf = path.join(__dirname, '../config/cfIPs.json')
|
const cfConf = path.resolve('./config/cfIPs.json')
|
||||||
const refreshInterval = 24 * 60 * 60 * 1000
|
const refreshInterval = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
const getIps = str => {
|
const getIps = str => {
|
||||||
@@ -32,8 +31,8 @@ const getCfIps = async app => {
|
|||||||
app.set('trust proxy', [...ips, ...cfIps])
|
app.set('trust proxy', [...ips, ...cfIps])
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = app => {
|
module.exports = (app, cloudflare = false) => {
|
||||||
if (!app.get('trustCloudflare')) {
|
if (!cloudflare) {
|
||||||
return app.set('trust proxy', ips)
|
return app.set('trust proxy', ips)
|
||||||
}
|
}
|
||||||
fs.readFile(cfConf, async (err, buff) => {
|
fs.readFile(cfConf, async (err, buff) => {
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user