commit 2c90d2e7dd0a01ed9caeaf025e6e0518531e8e27 Author: JJ Kasper Date: Thu May 17 14:31:05 2018 -0500 initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e717f5e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..03d8167 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "env": { + "es6": true, + "node": true, + "mocha": true + }, + "parserOptions": { + "ecmaVersion": 2017 + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ] + } +} diff --git a/.eslintrc.react.json b/.eslintrc.react.json new file mode 100644 index 0000000..4fa6d01 --- /dev/null +++ b/.eslintrc.react.json @@ -0,0 +1,43 @@ +{ + "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, + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4bd9e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ +# Logs +logs +*.log + +# Next build data +.next + +# cloudflare ips cache file +config/cfIPs.json + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Users Environment Variables +.lock-wscript + +# IDEs and editors (shamelessly copied from @angular/cli's .gitignore) +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Others +lib/ +db/ +kb/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d13cc4b --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bfc6a13 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# MYKB + +> A file system/markdown based knowledge base editor/viewer + +## Demo + +You can try the demo at: https://mykb.jjsweb.site + +\- Login +email: notadmin +password: secretpass + +P.S. the demo is reset every 10 minutes + +## 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) + +Current features: + +- live preview when editing a doc +- live file system watching +- caching of docs to speed up searching/viewing of docs +- automatic git versioning +- automatic trusting of cloudflare reverse proxies + +## Installing + +Getting up and running is as easy as 1, 2, 3 + +1. Clone repo + ``` + git clone https://github.com/ijjk/mykb + ``` +2. Install dependencies (omit `--prod` if developing) + ``` + cd path/to/mykb; npm i --prod + ``` +3. Start it + ``` + npm start + ``` + +## Options + +host.json + +| Name | Description | +| ---- | ----------- | +| host | The host to listen on | +| port | The port to listen on | +| basePath | Used to prefix all urls for reverse proxies | + +production.json (overrides default.json with production NODE_ENV var) + +| Name | Description | +| ---- | ----------- | +| 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 | +| cacheSize | Max size of docs to store in memory for faster searching (default 7.5MB) | +| 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) + +## Testing + +Simply run `npm test` and all your tests in the `test/` directory will be run + +## Linting + +Lint just react stuff +``` +npm lint:react +``` + +Lint just server stuff +``` +npm lint:node +``` + +Lint both +``` +npm lint +``` + +## Changelog + +__0.1.0__ + +- Initial release + +## License + +Copyright (c) 2017 + +Licensed under the [MIT license](LICENSE). \ No newline at end of file diff --git a/comps/AddDoc.js b/comps/AddDoc.js new file mode 100644 index 0000000..69f28e5 --- /dev/null +++ b/comps/AddDoc.js @@ -0,0 +1,49 @@ +import { 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, state) { + const { found, id, doc } = nextProps; + if(state.found !== found) return { found, id, doc }; + return null; + } + async componentDidUpdate(prevProps) { + const { user, found, id } = this.props; + if(prevProps.user.email === user.email || found) return; + if(!user.email) return; + this.setState(await getDoc(id)); + } + render() { + return ; + } + } + return connect(mapUser)(DocComp); +}; \ No newline at end of file diff --git a/comps/CodeMirror.js b/comps/CodeMirror.js new file mode 100644 index 0000000..c99b6ee --- /dev/null +++ b/comps/CodeMirror.js @@ -0,0 +1,65 @@ +import { 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 ( +
+