mirror of
https://github.com/Luzifer/wiki.git
synced 2024-12-20 10:31:18 +00:00
Port frontend to Vue 3 / Bootstrap 5.3
and update dependencies Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
e1039e5ae2
commit
a70eb9306a
19 changed files with 4334 additions and 8409 deletions
152
.eslintrc.js
Normal file
152
.eslintrc.js
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
/*
|
||||||
|
* Hack to automatically load globally installed eslint modules
|
||||||
|
* on Archlinux systems placed in /usr/lib/node_modules
|
||||||
|
*
|
||||||
|
* Source: https://github.com/eslint/eslint/issues/11914#issuecomment-569108633
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Module = require('module')
|
||||||
|
|
||||||
|
const hacks = [
|
||||||
|
'@babel/eslint-parser',
|
||||||
|
'eslint-plugin-vue',
|
||||||
|
]
|
||||||
|
|
||||||
|
const ModuleFindPath = Module._findPath
|
||||||
|
Module._findPath = (request, paths, isMain) => {
|
||||||
|
const r = ModuleFindPath(request, paths, isMain)
|
||||||
|
if (!r && hacks.includes(request)) {
|
||||||
|
return require.resolve(`/usr/lib/node_modules/${request}`)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ESLint configuration derived as differences from eslint:recommended
|
||||||
|
* with changes I found useful to ensure code quality and equal formatting
|
||||||
|
* https://eslint.org/docs/user-guide/configuring
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/recommended',
|
||||||
|
'eslint:recommended', // https://eslint.org/docs/rules/
|
||||||
|
],
|
||||||
|
|
||||||
|
globals: {
|
||||||
|
process: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
parser: '@babel/eslint-parser',
|
||||||
|
requireConfigFile: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
// required to lint *.vue files
|
||||||
|
'vue',
|
||||||
|
],
|
||||||
|
|
||||||
|
reportUnusedDisableDirectives: true,
|
||||||
|
|
||||||
|
root: true,
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'array-bracket-newline': ['error', { multiline: true }],
|
||||||
|
'array-bracket-spacing': ['error'],
|
||||||
|
'arrow-body-style': ['error', 'as-needed'],
|
||||||
|
'arrow-parens': ['error', 'as-needed'],
|
||||||
|
'arrow-spacing': ['error', { after: true, before: true }],
|
||||||
|
'block-spacing': ['error'],
|
||||||
|
'brace-style': ['error', '1tbs'],
|
||||||
|
'camelcase': ['error'],
|
||||||
|
'comma-dangle': ['error', 'always-multiline'],
|
||||||
|
'comma-spacing': ['error'],
|
||||||
|
'comma-style': ['error', 'last'],
|
||||||
|
'curly': ['error'],
|
||||||
|
'default-case-last': ['error'],
|
||||||
|
'default-param-last': ['error'],
|
||||||
|
'dot-location': ['error', 'property'],
|
||||||
|
'dot-notation': ['error'],
|
||||||
|
'eol-last': ['error', 'always'],
|
||||||
|
'eqeqeq': ['error', 'always', { null: 'ignore' }],
|
||||||
|
'func-call-spacing': ['error', 'never'],
|
||||||
|
'function-paren-newline': ['error', 'multiline'],
|
||||||
|
'generator-star-spacing': ['off'], // allow async-await
|
||||||
|
'implicit-arrow-linebreak': ['error'],
|
||||||
|
'indent': ['error', 2],
|
||||||
|
'key-spacing': ['error', { afterColon: true, beforeColon: false, mode: 'strict' }],
|
||||||
|
'keyword-spacing': ['error'],
|
||||||
|
'linebreak-style': ['error', 'unix'],
|
||||||
|
'lines-between-class-members': ['error'],
|
||||||
|
'multiline-comment-style': ['warn'],
|
||||||
|
'newline-per-chained-call': ['error'],
|
||||||
|
'no-alert': ['error'],
|
||||||
|
'no-console': ['off'],
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', // allow debugger during development
|
||||||
|
'no-duplicate-imports': ['error'],
|
||||||
|
'no-else-return': ['error'],
|
||||||
|
'no-empty-function': ['error'],
|
||||||
|
'no-extra-parens': ['error'],
|
||||||
|
'no-implicit-coercion': ['error'],
|
||||||
|
'no-lonely-if': ['error'],
|
||||||
|
'no-multi-spaces': ['error'],
|
||||||
|
'no-multiple-empty-lines': ['warn', { max: 2, maxBOF: 0, maxEOF: 0 }],
|
||||||
|
'no-promise-executor-return': ['error'],
|
||||||
|
'no-return-assign': ['error'],
|
||||||
|
'no-script-url': ['error'],
|
||||||
|
'no-template-curly-in-string': ['error'],
|
||||||
|
'no-trailing-spaces': ['error'],
|
||||||
|
'no-unneeded-ternary': ['error'],
|
||||||
|
'no-unreachable-loop': ['error'],
|
||||||
|
'no-unsafe-optional-chaining': ['error'],
|
||||||
|
'no-useless-return': ['error'],
|
||||||
|
'no-var': ['error'],
|
||||||
|
'no-warning-comments': ['error'],
|
||||||
|
'no-whitespace-before-property': ['error'],
|
||||||
|
'object-curly-newline': ['error', { consistent: true }],
|
||||||
|
'object-curly-spacing': ['error', 'always'],
|
||||||
|
'object-shorthand': ['error'],
|
||||||
|
'padded-blocks': ['error', 'never'],
|
||||||
|
'prefer-arrow-callback': ['error'],
|
||||||
|
'prefer-const': ['error'],
|
||||||
|
'prefer-object-spread': ['error'],
|
||||||
|
'prefer-rest-params': ['error'],
|
||||||
|
'prefer-template': ['error'],
|
||||||
|
'quote-props': ['error', 'consistent-as-needed', { keywords: false }],
|
||||||
|
'quotes': ['error', 'single', { allowTemplateLiterals: true }],
|
||||||
|
'require-atomic-updates': ['error'],
|
||||||
|
'require-await': ['error'],
|
||||||
|
'semi': ['error', 'never'],
|
||||||
|
'sort-imports': ['error', { ignoreCase: true, ignoreDeclarationSort: false, ignoreMemberSort: false }],
|
||||||
|
'sort-keys': ['error', 'asc', { caseSensitive: true, natural: false }],
|
||||||
|
'space-before-blocks': ['error', 'always'],
|
||||||
|
'space-before-function-paren': ['error', 'never'],
|
||||||
|
'space-in-parens': ['error', 'never'],
|
||||||
|
'space-infix-ops': ['error'],
|
||||||
|
'space-unary-ops': ['error', { nonwords: false, words: true }],
|
||||||
|
'spaced-comment': ['warn', 'always'],
|
||||||
|
'switch-colon-spacing': ['error'],
|
||||||
|
'template-curly-spacing': ['error', 'never'],
|
||||||
|
'unicode-bom': ['error', 'never'],
|
||||||
|
'vue/new-line-between-multi-line-property': ['error'],
|
||||||
|
'vue/no-empty-component-block': ['error'],
|
||||||
|
'vue/no-reserved-component-names': ['error'],
|
||||||
|
'vue/no-template-target-blank': ['error'],
|
||||||
|
'vue/no-unused-properties': ['error'],
|
||||||
|
'vue/no-unused-refs': ['error'],
|
||||||
|
'vue/no-useless-mustaches': ['error'],
|
||||||
|
'vue/order-in-components': ['off'], // Collides with sort-keys
|
||||||
|
'vue/require-name-property': ['error'],
|
||||||
|
'vue/v-for-delimiter-style': ['error'],
|
||||||
|
'vue/v-on-function-call': ['error'],
|
||||||
|
'wrap-iife': ['error'],
|
||||||
|
'yoda': ['error'],
|
||||||
|
},
|
||||||
|
}
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,3 @@
|
||||||
data
|
data
|
||||||
src/node_modules
|
node_modules
|
||||||
wiki
|
wiki
|
||||||
|
|
30
ci/build.mjs
Normal file
30
ci/build.mjs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import esbuild from 'esbuild'
|
||||||
|
import { sassPlugin } from 'esbuild-sass-plugin'
|
||||||
|
import vuePlugin from 'esbuild-plugin-vue3'
|
||||||
|
|
||||||
|
esbuild.build({
|
||||||
|
assetNames: '[name]-[hash]',
|
||||||
|
bundle: true,
|
||||||
|
define: {
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'dev'),
|
||||||
|
},
|
||||||
|
entryPoints: ['src/main.js'],
|
||||||
|
legalComments: 'none',
|
||||||
|
loader: {
|
||||||
|
'.ttf': 'file',
|
||||||
|
'.woff2': 'file',
|
||||||
|
},
|
||||||
|
minify: true,
|
||||||
|
outfile: 'frontend/app.js',
|
||||||
|
plugins: [
|
||||||
|
sassPlugin(),
|
||||||
|
vuePlugin(),
|
||||||
|
],
|
||||||
|
target: [
|
||||||
|
'chrome87',
|
||||||
|
'edge87',
|
||||||
|
'es2020',
|
||||||
|
'firefox84',
|
||||||
|
'safari14',
|
||||||
|
],
|
||||||
|
})
|
1
frontend/app.css
Normal file
1
frontend/app.css
Normal file
File diff suppressed because one or more lines are too long
308
frontend/app.js
308
frontend/app.js
File diff suppressed because one or more lines are too long
|
@ -1,21 +1,24 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" data-bs-theme="dark">
|
||||||
<meta charset="utf-8">
|
<head>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<title>Wiki</title>
|
<title>Wiki</title>
|
||||||
|
|
||||||
<div id="app"></div>
|
<link rel="stylesheet" href="/app.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.10.1/css/all.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
<!-- Loading prism from external not to pack ALL languages -->
|
<body>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css">
|
<div id="app"></div>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-okaidia.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/line-numbers/prism-line-numbers.min.js"></script>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.10.1/css/all.css">
|
<!-- Loading prism from external not to pack ALL languages -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-okaidia.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/line-numbers/prism-line-numbers.min.js"></script>
|
||||||
|
|
||||||
<script src="/app.js"></script>
|
<script src="/app.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
2
main.go
2
main.go
|
@ -88,7 +88,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleIndexPage(w http.ResponseWriter, r *http.Request) {
|
func handleIndexPage(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/app.js" {
|
if r.URL.Path != "/app.js" && r.URL.Path != "/app.css" {
|
||||||
r.URL.Path = "/index.html"
|
r.URL.Path = "/index.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3553
package-lock.json
generated
Normal file
3553
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
19
package.json
Normal file
19
package.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/eslint-parser": "^7.23.3",
|
||||||
|
"esbuild": "^0.19.11",
|
||||||
|
"esbuild-plugin-vue3": "^0.4.0",
|
||||||
|
"esbuild-sass-plugin": "^2.16.1",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-plugin-vue": "^9.20.1"
|
||||||
|
},
|
||||||
|
"name": "wiki",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^5.3.2",
|
||||||
|
"easymde": "^2.18.0",
|
||||||
|
"showdown": "^2.1.0",
|
||||||
|
"vue": "^3.4.14",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,88 +0,0 @@
|
||||||
// https://eslint.org/docs/user-guide/configuring
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
'root': true,
|
|
||||||
'parserOptions': {
|
|
||||||
parser: 'babel-eslint',
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
'env': {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
'extends': [
|
|
||||||
/*
|
|
||||||
* https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
|
|
||||||
* consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
|
|
||||||
*/
|
|
||||||
'plugin:vue/recommended',
|
|
||||||
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
|
|
||||||
'eslint:recommended',
|
|
||||||
],
|
|
||||||
// required to lint *.vue files
|
|
||||||
'plugins': ['vue'],
|
|
||||||
'globals': {
|
|
||||||
process: true,
|
|
||||||
Prism: true,
|
|
||||||
},
|
|
||||||
// add your custom rules here
|
|
||||||
'rules': {
|
|
||||||
'array-bracket-newline': ['error', { multiline: true }],
|
|
||||||
'array-bracket-spacing': ['error'],
|
|
||||||
'arrow-body-style': ['error', 'as-needed'],
|
|
||||||
'arrow-parens': ['error', 'as-needed'],
|
|
||||||
'arrow-spacing': ['error', { before: true, after: true }],
|
|
||||||
'block-spacing': ['error'],
|
|
||||||
'brace-style': ['error', '1tbs'],
|
|
||||||
'comma-dangle': ['error', 'always-multiline'], // Apply Contentflow rules
|
|
||||||
'comma-spacing': ['error'],
|
|
||||||
'comma-style': ['error', 'last'],
|
|
||||||
'curly': ['error'],
|
|
||||||
'dot-location': ['error', 'property'],
|
|
||||||
'dot-notation': ['error'],
|
|
||||||
'eol-last': ['error', 'always'],
|
|
||||||
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
|
|
||||||
'func-call-spacing': ['error', 'never'],
|
|
||||||
'function-paren-newline': ['error', 'multiline'],
|
|
||||||
'generator-star-spacing': ['off'], // allow async-await
|
|
||||||
'implicit-arrow-linebreak': ['error'],
|
|
||||||
'indent': ['error', 2],
|
|
||||||
'key-spacing': ['error', { beforeColon: false, afterColon: true, mode: 'strict' }],
|
|
||||||
'keyword-spacing': ['error'],
|
|
||||||
'linebreak-style': ['error', 'unix'],
|
|
||||||
'lines-between-class-members': ['error'],
|
|
||||||
'multiline-comment-style': ['warn'],
|
|
||||||
'newline-per-chained-call': ['error'],
|
|
||||||
'no-console': ['off'],
|
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', // allow debugger during development
|
|
||||||
'no-else-return': ['error'],
|
|
||||||
'no-extra-parens': ['error'],
|
|
||||||
'no-implicit-coercion': ['error'],
|
|
||||||
'no-lonely-if': ['error'],
|
|
||||||
'no-multiple-empty-lines': ['warn', { max: 2, maxEOF: 0, maxBOF: 0 }],
|
|
||||||
'no-multi-spaces': ['error'],
|
|
||||||
'no-trailing-spaces': ['error'],
|
|
||||||
'no-unneeded-ternary': ['error'],
|
|
||||||
'no-useless-return': ['error'],
|
|
||||||
'no-whitespace-before-property': ['error'],
|
|
||||||
'object-curly-newline': ['error', { consistent: true }],
|
|
||||||
'object-curly-spacing': ['error', 'always'],
|
|
||||||
'object-shorthand': ['error'],
|
|
||||||
'padded-blocks': ['error', 'never'],
|
|
||||||
'prefer-arrow-callback': ['error'],
|
|
||||||
'prefer-const': ['error'],
|
|
||||||
'prefer-object-spread': ['error'],
|
|
||||||
'prefer-template': ['error'],
|
|
||||||
'quote-props': ['error', 'consistent-as-needed', { keywords: true }],
|
|
||||||
'quotes': ['error', 'single', { allowTemplateLiterals: true }],
|
|
||||||
'semi': ['error', 'never'],
|
|
||||||
'space-before-blocks': ['error', 'always'],
|
|
||||||
'spaced-comment': ['warn', 'always'],
|
|
||||||
'space-infix-ops': ['error'],
|
|
||||||
'space-in-parens': ['error', 'never'],
|
|
||||||
'space-unary-ops': ['error', { words: true, nonwords: false }],
|
|
||||||
'switch-colon-spacing': ['error'],
|
|
||||||
'unicode-bom': ['error', 'never'],
|
|
||||||
'wrap-iife': ['error'],
|
|
||||||
'yoda': ['error'],
|
|
||||||
},
|
|
||||||
}
|
|
100
src/app.vue
100
src/app.vue
|
@ -1,76 +1,76 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<b-navbar
|
<nav class="navbar navbar-expand-lg bg-body-tertiary mb-3">
|
||||||
type="dark"
|
<div class="container-fluid">
|
||||||
variant="primary"
|
<router-link
|
||||||
class="mb-4"
|
class="navbar-brand"
|
||||||
>
|
:to="{ name: 'home' }"
|
||||||
<b-navbar-brand
|
|
||||||
href="/"
|
|
||||||
@click.prevent="$router.push({ name: 'home' })"
|
|
||||||
>
|
|
||||||
Wiki
|
|
||||||
</b-navbar-brand>
|
|
||||||
|
|
||||||
<b-navbar-nav>
|
|
||||||
<b-nav-item
|
|
||||||
v-for="page in navContent"
|
|
||||||
:key="page"
|
|
||||||
:to="{ name: 'view', params: { page } }"
|
|
||||||
>
|
>
|
||||||
{{ page }}
|
Wiki
|
||||||
</b-nav-item>
|
</router-link>
|
||||||
</b-navbar-nav>
|
<button
|
||||||
|
class="navbar-toggler"
|
||||||
<template ref="nav">
|
type="button"
|
||||||
<vue-markdown
|
data-bs-toggle="collapse"
|
||||||
class="nav"
|
data-bs-target="#navbarSupportedContent"
|
||||||
:source="navContent"
|
aria-controls="navbarSupportedContent"
|
||||||
:prerender="prerender"
|
aria-expanded="false"
|
||||||
@rendered="rendered"
|
aria-label="Toggle navigation"
|
||||||
/>
|
>
|
||||||
</template>
|
<span class="navbar-toggler-icon" />
|
||||||
</b-navbar>
|
</button>
|
||||||
|
<div
|
||||||
|
id="navbarSupportedContent"
|
||||||
|
class="collapse navbar-collapse"
|
||||||
|
>
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li
|
||||||
|
v-for="page in navContent"
|
||||||
|
:key="page"
|
||||||
|
class="nav-item"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
class="nav-link"
|
||||||
|
:to="{ name: 'view', params: { page } }"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
|
||||||
import VueMarkdown from 'vue-markdown'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
VueMarkdown,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
navContent: '',
|
navContent: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.loadNav()
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
loadNav() {
|
loadNav() {
|
||||||
axios.get(`/_content/_navigation`)
|
return fetch(`/_content/_navigation`)
|
||||||
.then(resp => {
|
.then(resp => resp.json())
|
||||||
this.navContent = resp.data.content.split('\n')
|
.then(data => {
|
||||||
.filter(el => !el.match(/^!/))
|
this.navContent = data.content.split('\n')
|
||||||
|
.filter(el => el && !el.match(/^!/))
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (err.response && err.response.status === 404) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.loadNav()
|
||||||
|
},
|
||||||
|
|
||||||
|
name: 'WikiApp',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
82
src/easymde.css
Normal file
82
src/easymde.css
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Custom CSS to make EasyMDE play nice with Bootstrap colors
|
||||||
|
* https://github.com/Ionaru/easy-markdown-editor/issues/131
|
||||||
|
*/
|
||||||
|
|
||||||
|
.EasyMDEContainer .CodeMirror {
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
border-color: var(--bs-border-color);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .cm-s-easymde .CodeMirror-cursor {
|
||||||
|
border-color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
border-left: 1px solid var(--bs-body-color);
|
||||||
|
border-right: none;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .editor-toolbar>* {
|
||||||
|
border-color: var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
border-top: 1px solid var(--bs-border-color);
|
||||||
|
border-left: 1px solid var(--bs-border-color);
|
||||||
|
border-right: 1px solid var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar i.separator {
|
||||||
|
border-left: 1px solid var(--bs-border-color);
|
||||||
|
border-right: 1px solid var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .editor-toolbar>.active,
|
||||||
|
.editor-toolbar>button:hover,
|
||||||
|
.editor-preview pre,
|
||||||
|
.cm-s-easymde .cm-comment {
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .CodeMirror-fullscreen {
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar.fullscreen {
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-preview {
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-preview-side {
|
||||||
|
border-color: var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-selected {
|
||||||
|
background: var(--bs-secondary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-focused .CodeMirror-selected {
|
||||||
|
background: var(--bs-secondary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-line::selection,
|
||||||
|
.CodeMirror-line>span::selection,
|
||||||
|
.CodeMirror-line>span>span::selection {
|
||||||
|
background: var(--bs-secondary-bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-line::-moz-selection,
|
||||||
|
.CodeMirror-line>span::-moz-selection,
|
||||||
|
.CodeMirror-line>span>span::-moz-selection {
|
||||||
|
background: var(--bs-secondary-bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .CodeMirror-focused .CodeMirror-selected {
|
||||||
|
background: var(--bs-secondary-bg)
|
||||||
|
}
|
123
src/edit.vue
123
src/edit.vue
|
@ -1,33 +1,84 @@
|
||||||
<template>
|
<template>
|
||||||
<b-container>
|
<div class="container">
|
||||||
<b-row>
|
<div class="row">
|
||||||
<b-col>
|
<div class="col">
|
||||||
<textarea ref="editor" />
|
<textarea ref="editor" />
|
||||||
|
|
||||||
<b-btn
|
<button
|
||||||
variant="primary"
|
class="btn btn-primary"
|
||||||
@click="save"
|
@click="save"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</b-btn>
|
</button>
|
||||||
</b-col>
|
</div>
|
||||||
</b-row>
|
</div>
|
||||||
</b-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
|
||||||
import EasyMDE from 'easymde'
|
import EasyMDE from 'easymde'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Edit',
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: null,
|
editor: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
loadPage(pageName) {
|
||||||
|
console.debug(`Loading ${pageName}...`)
|
||||||
|
return fetch(`/_content/${pageName}`)
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.status === 404) {
|
||||||
|
return { content: `# ${pageName}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.json()
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.toTextArea()
|
||||||
|
this.editor = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor = new EasyMDE({
|
||||||
|
element: this.$refs.editor,
|
||||||
|
forceSync: true,
|
||||||
|
indentWithTabs: false,
|
||||||
|
initialValue: data.content,
|
||||||
|
})
|
||||||
|
|
||||||
|
// this.editor.codemirror.setValue(data.content)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.response && err.response.status === 404) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
return fetch(`/_content/${this.$route.params.page}`, {
|
||||||
|
body: JSON.stringify({ content: this.$refs.editor.value }),
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$router.push({ name: 'view', params: { page: this.$route.params.page } })
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.loadPage(this.$route.params.page)
|
||||||
|
},
|
||||||
|
|
||||||
|
name: 'WikiEdit',
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
'$route'(to, from) {
|
'$route'(to, from) {
|
||||||
if (to.params.page === from.params.page) {
|
if (to.params.page === from.params.page) {
|
||||||
|
@ -37,53 +88,5 @@ export default {
|
||||||
this.loadPage(to.params.page)
|
this.loadPage(to.params.page)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
|
||||||
if (!this.editor) {
|
|
||||||
this.editor = new EasyMDE({
|
|
||||||
element: this.$refs.editor,
|
|
||||||
forceSync: true,
|
|
||||||
indentWithTabs: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
window.editor = this.editor
|
|
||||||
}
|
|
||||||
this.loadPage(this.$route.params.page)
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
loadPage(pageName) {
|
|
||||||
console.debug(`Loading ${pageName}...`)
|
|
||||||
axios.get(`/_content/${pageName}`)
|
|
||||||
.then(resp => {
|
|
||||||
this.editor.codemirror.setValue(resp.data.content)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (err.response && err.response.status === 404) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.error(err)
|
|
||||||
// FIXME: Show error
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
save() {
|
|
||||||
axios.post(`/_content/${this.$route.params.page}`, {
|
|
||||||
content: this.$refs.editor.value,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.$router.push({ name: 'view', params: { page: this.$route.params.page } })
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.editor-toolbar {
|
|
||||||
background-color: #ccc;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
33
src/main.js
33
src/main.js
|
@ -1,19 +1,13 @@
|
||||||
import Vue from 'vue'
|
|
||||||
import VueRouter from 'vue-router'
|
|
||||||
|
|
||||||
import BootstrapVue from 'bootstrap-vue'
|
|
||||||
|
|
||||||
import 'bootstrap/dist/css/bootstrap.css'
|
import 'bootstrap/dist/css/bootstrap.css'
|
||||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
|
||||||
import 'bootswatch/dist/darkly/bootstrap.css'
|
|
||||||
import 'easymde/dist/easymde.min.css'
|
import 'easymde/dist/easymde.min.css'
|
||||||
|
import './easymde.css'
|
||||||
|
|
||||||
|
import { createApp, h } from 'vue'
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
import App from './app.vue'
|
import App from './app.vue'
|
||||||
import View from './view.vue'
|
|
||||||
import Edit from './edit.vue'
|
import Edit from './edit.vue'
|
||||||
|
import View from './view.vue'
|
||||||
Vue.use(BootstrapVue)
|
|
||||||
Vue.use(VueRouter)
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
@ -33,15 +27,20 @@ const routes = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
new Vue({
|
const app = createApp({
|
||||||
el: '#app',
|
name: 'WikiMain',
|
||||||
components: { App },
|
render() {
|
||||||
data: { },
|
return h(App)
|
||||||
render: createElement => createElement('app'),
|
},
|
||||||
|
|
||||||
router,
|
router,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.mount('#app')
|
||||||
|
|
45
src/markdown.vue
Normal file
45
src/markdown.vue
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div v-html="render" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import showdown from 'showdown'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
render: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['rendered'],
|
||||||
|
|
||||||
|
name: 'WikiMarkdown',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
content: {
|
||||||
|
default: '',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
prerender: {
|
||||||
|
default: null,
|
||||||
|
type: Function,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
content(to) {
|
||||||
|
let content = to
|
||||||
|
if (this.prerender) {
|
||||||
|
content = this.prerender(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
const converter = new showdown.Converter()
|
||||||
|
this.render = converter.makeHtml(content)
|
||||||
|
this.$emit('rendered')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
7985
src/package-lock.json
generated
7985
src/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
"devDependencies": {
|
|
||||||
"babel-core": "^6.26.3",
|
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"babel-loader": "^7.1.5",
|
|
||||||
"babel-preset-env": "^1.7.0",
|
|
||||||
"css-loader": "^1.0.0",
|
|
||||||
"easymde": "^2.11.0",
|
|
||||||
"eslint": "^5.13.0",
|
|
||||||
"eslint-config-standard": "^12.0.0",
|
|
||||||
"eslint-friendly-formatter": "^4.0.1",
|
|
||||||
"eslint-loader": "^2.1.1",
|
|
||||||
"eslint-plugin-import": "^2.22.0",
|
|
||||||
"eslint-plugin-node": "^8.0.1",
|
|
||||||
"eslint-plugin-promise": "^4.0.1",
|
|
||||||
"eslint-plugin-standard": "^4.0.1",
|
|
||||||
"eslint-plugin-vue": "^5.2.3",
|
|
||||||
"node-sass": "^4.14.1",
|
|
||||||
"sass-loader": "^7.3.1",
|
|
||||||
"style-loader": "^0.21.0",
|
|
||||||
"vue-loader": "^15.9.3",
|
|
||||||
"vue-markdown": "^2.2.4",
|
|
||||||
"vue-router": "^3.3.4",
|
|
||||||
"vue-template-compiler": "^2.6.11",
|
|
||||||
"webpack": "^4.44.1",
|
|
||||||
"webpack-cli": "^3.3.12",
|
|
||||||
"webpack-dev-middleware": "^1.4.0",
|
|
||||||
"webpack-hot-middleware": "^2.9.1"
|
|
||||||
},
|
|
||||||
"name": "wiki",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack -p"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^0.19.2",
|
|
||||||
"bootstrap": "^4.5.0",
|
|
||||||
"bootstrap-vue": "^2.16.0",
|
|
||||||
"bootswatch": "^4.5.0",
|
|
||||||
"popper.js": "^1.16.1",
|
|
||||||
"vue": "^2.6.11"
|
|
||||||
}
|
|
||||||
}
|
|
90
src/view.vue
90
src/view.vue
|
@ -1,37 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<b-container>
|
<div class="container">
|
||||||
<b-row>
|
<div class="row">
|
||||||
<b-col
|
<div
|
||||||
ref="content"
|
ref="content"
|
||||||
class="relAnchor"
|
class="col relAnchor"
|
||||||
>
|
>
|
||||||
<b-btn
|
<router-link
|
||||||
class="editBtn"
|
v-if="$route.params.page"
|
||||||
variant="secondary"
|
class="btn btn-secondary btn-sm editBtn"
|
||||||
size="sm"
|
|
||||||
:to="{ name: 'edit', params: { page: $route.params.page } }"
|
:to="{ name: 'edit', params: { page: $route.params.page } }"
|
||||||
>
|
>
|
||||||
<i class="fas fa-edit" />
|
<i class="fas fa-edit" />
|
||||||
</b-btn>
|
</router-link>
|
||||||
<vue-markdown
|
|
||||||
:source="content"
|
<md-render
|
||||||
|
:content="content"
|
||||||
:prerender="prerender"
|
:prerender="prerender"
|
||||||
@rendered="rendered"
|
@rendered="rendered"
|
||||||
/>
|
/>
|
||||||
</b-col>
|
</div>
|
||||||
</b-row>
|
</div>
|
||||||
</b-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
/* global Prism */
|
||||||
import VueMarkdown from 'vue-markdown'
|
|
||||||
|
import mdRender from './markdown.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'View',
|
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
VueMarkdown,
|
mdRender,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -40,20 +40,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
'$route'(to, from) {
|
|
||||||
if (to.params.page === from.params.page) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadPage(to.params.page)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.loadPage(this.$route.params.page)
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
intLinkClick(evt) {
|
intLinkClick(evt) {
|
||||||
const link = evt.target
|
const link = evt.target
|
||||||
|
@ -63,23 +49,33 @@ export default {
|
||||||
|
|
||||||
loadPage(pageName) {
|
loadPage(pageName) {
|
||||||
console.debug(`Loading ${pageName}...`)
|
console.debug(`Loading ${pageName}...`)
|
||||||
axios.get(`/_content/${pageName}`)
|
return fetch(`/_content/${pageName}`)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
this.content = resp.data.content
|
if (resp.status === 404) {
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (err.response && err.response.status === 404) {
|
|
||||||
this.$router.push({ name: 'edit', params: { page: pageName } })
|
this.$router.push({ name: 'edit', params: { page: pageName } })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resp.json()
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (!data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.content = data.content
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
// FIXME: Show error
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
prerender(mdtext) {
|
prerender(mdtext) {
|
||||||
// replace [[Internal]] links
|
// replace [[Internal]] links
|
||||||
mdtext = mdtext.replace(new RegExp(/\[\[([^\]]+)\]\]/, 'g'), '<a class="intLink" data-page="$1" href="$1">$1</a>')
|
mdtext = mdtext.replace(
|
||||||
|
new RegExp(/\[\[([^\]]+)\]\]/, 'g'),
|
||||||
|
'<a class="intLink" data-page="$1" href="$1">$1</a>',
|
||||||
|
)
|
||||||
|
|
||||||
return mdtext
|
return mdtext
|
||||||
},
|
},
|
||||||
|
@ -98,6 +94,22 @@ export default {
|
||||||
}, 100)
|
}, 100)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.loadPage(this.$route.params.page)
|
||||||
|
},
|
||||||
|
|
||||||
|
name: 'WikiView',
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'$route'(to, from) {
|
||||||
|
if (to.params.page === from.params.page) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadPage(to.params.page)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
const path = require('path')
|
|
||||||
const webpack = require('webpack')
|
|
||||||
|
|
||||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: './main.js',
|
|
||||||
output: {
|
|
||||||
filename: 'app.js',
|
|
||||||
path: path.resolve(__dirname, '..', 'frontend'),
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.optimize.OccurrenceOrderPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': {
|
|
||||||
NODE_ENV: JSON.stringify('production'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
new VueLoaderPlugin(),
|
|
||||||
],
|
|
||||||
optimization: {
|
|
||||||
minimize: true,
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
|
|
||||||
{
|
|
||||||
test: /\.(s?)css$/,
|
|
||||||
use: [
|
|
||||||
'style-loader',
|
|
||||||
'css-loader',
|
|
||||||
'sass-loader',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
exclude: /(node_modules|bower_components)/,
|
|
||||||
use: {
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
presets: [['env', { targets: { browsers: ['>0.25%', 'not ie 11', 'not op_mini all'] } }]],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
test: /\.vue$/,
|
|
||||||
loader: 'vue-loader',
|
|
||||||
},
|
|
||||||
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
Loading…
Reference in a new issue