1
0
mirror of https://github.com/Luzifer/wiki.git synced 2024-09-18 23:23:00 +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:
Knut Ahlers 2024-01-30 15:46:01 +01:00
parent e1039e5ae2
commit a70eb9306a
Signed by: luzifer
SSH Key Fingerprint: SHA256:/xtE5lCgiRDQr8SLxHMS92ZBlACmATUmF1crK16Ks4E
19 changed files with 4334 additions and 8409 deletions

152
.eslintrc.js Normal file
View 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
View File

@ -1,3 +1,3 @@
data data
src/node_modules node_modules
wiki wiki

30
ci/build.mjs Normal file
View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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>

View File

@ -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

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View 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"
}
}

View File

@ -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'],
},
}

View File

@ -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
View 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)
}

View File

@ -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>

View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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',
},
],
},
}