diff --git a/.eslintrc.json b/.eslintrc.json
index 72ce2be8..3c0701fc 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,12 +1,10 @@
{
"extends": "standard",
-
"env": {
"node": true,
"es6": true,
"jest": true
},
-
"globals": {
"document": false,
"navigator": false,
@@ -16,5 +14,8 @@
"ROOTPATH": true,
"SERVERPATH": true,
"IS_DEBUG": true
+ },
+ "rules": {
+ "space-before-function-paren": 0
}
}
diff --git a/client/js/app.js b/client/js/app.js
index ab93d206..246c890d 100644
--- a/client/js/app.js
+++ b/client/js/app.js
@@ -1,14 +1,64 @@
'use strict'
-/* global alertsData */
+/* global alertsData, siteLang */
+/* eslint-disable no-new */
import $ from 'jquery'
import _ from 'lodash'
+import Vue from 'vue'
+import Vuex from 'vuex'
import io from 'socket.io-client'
+import i18next from 'i18next'
+import i18nextXHR from 'i18next-xhr-backend'
+import VueI18Next from '@panter/vue-i18next'
import Alerts from './components/alerts.js'
import 'jquery-smooth-scroll'
import 'jquery-sticky'
+// ====================================
+// Load Vue Components
+// ====================================
+
+import anchorComponent from './components/anchor.vue'
+import colorPickerComponent from './components/color-picker.vue'
+import loadingSpinnerComponent from './components/loading-spinner.vue'
+import searchComponent from './components/search.vue'
+
+import adminProfileComponent from './pages/admin-profile.component.js'
+import adminSettingsComponent from './pages/admin-settings.component.js'
+
+// ====================================
+// Initialize i18next
+// ====================================
+
+Vue.use(VueI18Next)
+
+i18next
+ .use(i18nextXHR)
+ .init({
+ backend: {
+ loadPath: '/js/i18n/{{lng}}.json'
+ },
+ lng: siteLang,
+ fallbackLng: siteLang
+ })
+
+// ====================================
+// Initialize Vuex
+// ====================================
+
+Vue.use(Vuex)
+
+const store = new Vuex.Store({
+ state: {
+ loading: false
+ },
+ mutations: {
+ startLoading: state => { state.loading = true },
+ stopLoading: state => { state.loading = false }
+ }
+})
+
$(() => {
// ====================================
// Scroll
@@ -27,28 +77,47 @@ $(() => {
// ====================================
$(window).bind('beforeunload', () => {
- $('#notifload').addClass('active')
+ store.commit('startLoading')
})
$(document).ajaxSend(() => {
- $('#notifload').addClass('active')
+ store.commit('startLoading')
}).ajaxComplete(() => {
- $('#notifload').removeClass('active')
+ store.commit('stopLoading')
})
- var alerts = new Alerts()
+ var alerts = {}
+ /*var alerts = new Alerts()
if (alertsData) {
_.forEach(alertsData, (alertRow) => {
alerts.push(alertRow)
})
- }
+ }*/
// ====================================
// Establish WebSocket connection
// ====================================
- var socket = io(window.location.origin)
+ let socket = io(window.location.origin)
+ window.socket = socket
- require('./components/search.js')(socket)
+ // ====================================
+ // Bootstrap Vue
+ // ====================================
+
+ const i18n = new VueI18Next(i18next)
+ new Vue({
+ components: {
+ adminProfile: adminProfileComponent,
+ adminSettings: adminSettingsComponent,
+ anchor: anchorComponent,
+ colorPicker: colorPickerComponent,
+ loadingSpinner: loadingSpinnerComponent,
+ search: searchComponent
+ },
+ store,
+ i18n,
+ el: '#root'
+ })
// ====================================
// Pages logic
diff --git a/client/js/components/anchor.vue b/client/js/components/anchor.vue
new file mode 100644
index 00000000..72250aad
--- /dev/null
+++ b/client/js/components/anchor.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/client/js/components/color-picker.vue b/client/js/components/color-picker.vue
new file mode 100644
index 00000000..5e41a7f9
--- /dev/null
+++ b/client/js/components/color-picker.vue
@@ -0,0 +1,15 @@
+
+ p.control
+ input.input(type='text', placeholder='#F0F0F0', v-model='color')
+
+
+
diff --git a/client/js/components/copy-path.vue b/client/js/components/copy-path.vue
deleted file mode 100644
index 86567a54..00000000
--- a/client/js/components/copy-path.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
diff --git a/client/js/components/loading-spinner.vue b/client/js/components/loading-spinner.vue
new file mode 100644
index 00000000..d93d317f
--- /dev/null
+++ b/client/js/components/loading-spinner.vue
@@ -0,0 +1,12 @@
+
+ i.nav-item#notifload(v-bind:class='{ "is-active": loading }')
+
+
+
diff --git a/client/js/components/search.js b/client/js/components/search.js
deleted file mode 100644
index 1c6177a2..00000000
--- a/client/js/components/search.js
+++ /dev/null
@@ -1,87 +0,0 @@
-'use strict'
-
-import $ from 'jquery'
-import _ from 'lodash'
-import Vue from 'vue'
-
-module.exports = (socket) => {
- if ($('#search-input').length) {
- $('#search-input').focus()
-
- $('.searchresults').css('display', 'block')
-
- var vueHeader = new Vue({
- el: '#header-container',
- data: {
- searchq: '',
- searchres: [],
- searchsuggest: [],
- searchload: 0,
- searchactive: false,
- searchmoveidx: 0,
- searchmovekey: '',
- searchmovearr: []
- },
- watch: {
- searchq: (val, oldVal) => {
- vueHeader.searchmoveidx = 0
- if (val.length >= 3) {
- vueHeader.searchactive = true
- vueHeader.searchload++
- socket.emit('search', { terms: val }, (data) => {
- vueHeader.searchres = data.match
- vueHeader.searchsuggest = data.suggest
- vueHeader.searchmovearr = _.concat([], vueHeader.searchres, vueHeader.searchsuggest)
- if (vueHeader.searchload > 0) { vueHeader.searchload-- }
- })
- } else {
- vueHeader.searchactive = false
- vueHeader.searchres = []
- vueHeader.searchsuggest = []
- vueHeader.searchmovearr = []
- vueHeader.searchload = 0
- }
- },
- searchmoveidx: (val, oldVal) => {
- if (val > 0) {
- vueHeader.searchmovekey = (vueHeader.searchmovearr[val - 1])
- ? 'res.' + vueHeader.searchmovearr[val - 1].entryPath
- : 'sug.' + vueHeader.searchmovearr[val - 1]
- } else {
- vueHeader.searchmovekey = ''
- }
- }
- },
- methods: {
- useSuggestion: (sug) => {
- vueHeader.searchq = sug
- },
- closeSearch: () => {
- vueHeader.searchq = ''
- },
- moveSelectSearch: () => {
- if (vueHeader.searchmoveidx < 1) { return }
- let i = vueHeader.searchmoveidx - 1
-
- if (vueHeader.searchmovearr[i]) {
- window.location.assign('/' + vueHeader.searchmovearr[i].entryPath)
- } else {
- vueHeader.searchq = vueHeader.searchmovearr[i]
- }
- },
- moveDownSearch: () => {
- if (vueHeader.searchmoveidx < vueHeader.searchmovearr.length) {
- vueHeader.searchmoveidx++
- }
- },
- moveUpSearch: () => {
- if (vueHeader.searchmoveidx > 0) {
- vueHeader.searchmoveidx--
- }
- }
- }
- })
-
- $('main').on('click', vueHeader.closeSearch)
- }
-}
diff --git a/client/js/components/search.vue b/client/js/components/search.vue
new file mode 100644
index 00000000..1935512b
--- /dev/null
+++ b/client/js/components/search.vue
@@ -0,0 +1,101 @@
+
+ .nav-item
+ p.control(v-bind:class='{ "is-loading": searchload > 0 }')
+ input.input#search-input(type='text', v-model='searchq', autofocus, @keyup.esc='closeSearch', @keyup.down='moveDownSearch', @keyup.up='moveUpSearch', @keyup.enter='moveSelectSearch', debounce='400', v-bind:placeholder='$t("search.placeholder")')
+
+ transition(name='searchresults')
+ .searchresults(v-show='searchactive', v-cloak)
+ p.searchresults-label {{ $t('search.results') }}
+ ul.searchresults-list
+ li(v-if='searchres.length === 0')
+ a: em {{ $t('search.nomatch') }}
+ li(v-for='sres in searchres', v-bind:class='{ "is-active": searchmovekey === "res." + sres.entryPath }')
+ a(v-bind:href='"/" + sres.entryPath') {{ sres.title }}
+ p.searchresults-label(v-if='searchsuggest.length > 0') {{ $t('search.didyoumean') }}
+ ul.searchresults-list(v-if='searchsuggest.length > 0')
+ li(v-for='sug in searchsuggest', v-bind:class='{ "is-active": searchmovekey === "sug." + sug }')
+ a(v-on:click='useSuggestion(sug)') {{ sug }}
+
+
+
diff --git a/client/js/modals/create.vue b/client/js/modals/create.vue
new file mode 100644
index 00000000..5ae92a05
--- /dev/null
+++ b/client/js/modals/create.vue
@@ -0,0 +1,54 @@
+
+ .modal(v-if='isShown')
+ .modal-background
+ .modal-container
+ .modal-content
+ header.is-light-blue Create New Document
+ section
+ label.label Enter the new document path:
+ p.control.is-fullwidth(v-class='{ "is-loading": isLoading }')
+ input.input(type='text', placeholder='page-name', v-model='entrypath', autofocus)
+ span.help.is-danger(v-show='isInvalid') This document path is invalid!
+ footer
+ a.button.is-grey.is-outlined(v-on:click='hide') Discard
+ a.button.is-light-blue(v-on:click='create') Create
+
+
+
diff --git a/client/js/pages/admin-profile.component.js b/client/js/pages/admin-profile.component.js
new file mode 100644
index 00000000..4c4486b4
--- /dev/null
+++ b/client/js/pages/admin-profile.component.js
@@ -0,0 +1,30 @@
+'use strict'
+
+import * as $ from 'jquery'
+
+export default {
+ name: 'admin-profile',
+ props: ['email', 'name', 'provider'],
+ data() {
+ return {
+ password: '********',
+ passwordVerify: '********'
+ }
+ },
+ methods: {
+ saveUser() {
+ if (this.password !== this.passwordVerify) {
+ //alerts.pushError('Error', "Passwords don't match!")
+ return
+ }
+ $.post(window.location.href, {
+ password: this.password,
+ name: this.name
+ }).done((resp) => {
+ //alerts.pushSuccess('Saved successfully', 'Changes have been applied.')
+ }).fail((jqXHR, txtStatus, resp) => {
+ //alerts.pushError('Error', resp)
+ })
+ }
+ }
+}
diff --git a/client/js/pages/admin-settings.component.js b/client/js/pages/admin-settings.component.js
new file mode 100644
index 00000000..f21b3eb2
--- /dev/null
+++ b/client/js/pages/admin-settings.component.js
@@ -0,0 +1,52 @@
+'use strict'
+
+import * as $ from 'jquery'
+
+export default {
+ name: 'admin-settings',
+ data() {
+ return {
+ upgradeModal: {
+ state: false,
+ step: 'confirm',
+ mode: 'upgrade',
+ error: 'Something went wrong.'
+ }
+ }
+ },
+ methods: {
+ upgrade() {
+ this.upgradeModal.mode = 'upgrade'
+ this.upgradeModal.step = 'confirm'
+ this.upgradeModal.state = true
+ },
+ reinstall() {
+ this.upgradeModal.mode = 're-install'
+ this.upgradeModal.step = 'confirm'
+ this.upgradeModal.state = true
+ },
+ upgradeCancel() {
+ this.upgradeModal.state = false
+ },
+ upgradeStart() {
+ this.upgradeModal.step = 'running'
+ $.post('/admin/settings/install', {
+ mode: this.upgradeModal.mode
+ }).done((resp) => {
+ // todo
+ }).fail((jqXHR, txtStatus, resp) => {
+ this.upgradeModal.step = 'error'
+ this.upgradeModal.error = jqXHR.responseText
+ })
+ },
+ flushcache() {
+ window.alert('Coming soon!')
+ },
+ resetaccounts() {
+ window.alert('Coming soon!')
+ },
+ flushsessions() {
+ window.alert('Coming soon!')
+ }
+ }
+}
diff --git a/client/js/pages/admin.js b/client/js/pages/admin.js
index 08d0fa95..030d7c96 100644
--- a/client/js/pages/admin.js
+++ b/client/js/pages/admin.js
@@ -1,41 +1,13 @@
'use strict'
-/* global usrData, usrDataName */
+/* global usrData */
import $ from 'jquery'
import _ from 'lodash'
import Vue from 'vue'
module.exports = (alerts) => {
- if ($('#page-type-admin-profile').length) {
- let vueProfile = new Vue({
- el: '#page-type-admin-profile',
- data: {
- password: '********',
- passwordVerify: '********',
- name: ''
- },
- methods: {
- saveUser: (ev) => {
- if (vueProfile.password !== vueProfile.passwordVerify) {
- alerts.pushError('Error', "Passwords don't match!")
- return
- }
- $.post(window.location.href, {
- password: vueProfile.password,
- name: vueProfile.name
- }).done((resp) => {
- alerts.pushSuccess('Saved successfully', 'Changes have been applied.')
- }).fail((jqXHR, txtStatus, resp) => {
- alerts.pushError('Error', resp)
- })
- }
- },
- created: function () {
- this.name = usrDataName
- }
- })
- } else if ($('#page-type-admin-users').length) {
+ if ($('#page-type-admin-users').length) {
require('../modals/admin-users-create.js')(alerts)
} else if ($('#page-type-admin-users-edit').length) {
let vueEditUser = new Vue({
@@ -98,52 +70,5 @@ module.exports = (alerts) => {
}
})
require('../modals/admin-users-delete.js')(alerts)
- } else if ($('#page-type-admin-settings').length) {
- let vueSettings = new Vue({ // eslint-disable-line no-unused-vars
- el: '#page-type-admin-settings',
- data: {
- upgradeModal: {
- state: false,
- step: 'confirm',
- mode: 'upgrade',
- error: 'Something went wrong.'
- }
- },
- methods: {
- upgrade: (ev) => {
- vueSettings.upgradeModal.mode = 'upgrade'
- vueSettings.upgradeModal.step = 'confirm'
- vueSettings.upgradeModal.state = true
- },
- reinstall: (ev) => {
- vueSettings.upgradeModal.mode = 're-install'
- vueSettings.upgradeModal.step = 'confirm'
- vueSettings.upgradeModal.state = true
- },
- upgradeCancel: (ev) => {
- vueSettings.upgradeModal.state = false
- },
- upgradeStart: (ev) => {
- vueSettings.upgradeModal.step = 'running'
- $.post('/admin/settings/install', {
- mode: vueSettings.upgradeModal.mode
- }).done((resp) => {
- // todo
- }).fail((jqXHR, txtStatus, resp) => {
- vueSettings.upgradeModal.step = 'error'
- vueSettings.upgradeModal.error = jqXHR.responseText
- })
- },
- flushcache: (ev) => {
- window.alert('Coming soon!')
- },
- resetaccounts: (ev) => {
- window.alert('Coming soon!')
- },
- flushsessions: (ev) => {
- window.alert('Coming soon!')
- }
- }
- })
}
}
diff --git a/client/js/pages/view.js b/client/js/pages/view.js
index bde05222..0a6f4922 100644
--- a/client/js/pages/view.js
+++ b/client/js/pages/view.js
@@ -4,7 +4,7 @@
import $ from 'jquery'
import MathJax from 'mathjax'
-import * as CopyPath from '../components/copy-path.vue'
+// import * as CopyPath from '../components/copy-path.vue'
import Vue from 'vue'
module.exports = (alerts) => {
@@ -13,10 +13,10 @@ module.exports = (alerts) => {
// Copy Path
- new Vue({
- el: '.modal-copypath',
- render: h => h(CopyPath)
- })
+ // new Vue({
+ // el: '.modal-copypath',
+ // render: h => h(CopyPath)
+ // })
// MathJax Render
diff --git a/client/scss/components/search.scss b/client/scss/components/search.scss
index eafbef6d..fe30714e 100644
--- a/client/scss/components/search.scss
+++ b/client/scss/components/search.scss
@@ -1,47 +1,56 @@
.searchresults {
position: fixed;
- top: 45px;
+ top: 50px;
left: 0;
right: 0;
margin: 0 auto;
width: 500px;
z-index: 1;
- background-color: mc($primary, '700');
- border-bottom: 5px solid mc($primary, '800');
- box-shadow: 0 0 5px mc($primary, '500');
+ background-color: darken(mc('blue-grey', '900'), 2%);
+ border: 1px solid mc('blue-grey', '900');
+ box-shadow: 0 0 5px mc('blue-grey', '500');
color: #FFF;
+ transition: max-height 1s ease;
- &.slideInDown {
- @include prefix(animation-duration, .6s);
- }
+ &-enter-active, &-leave-active {
+ overflow: hidden;
+ }
+ &-enter-to, &-leave {
+ max-height: 500px;
+ }
+ &-enter, &-leave-to {
+ max-height: 0px;
+ }
.searchresults-label {
- color: mc($primary, '200');
- padding: 15px 10px 10px;
+ background-color: mc('blue-grey', '800');
+ color: mc('blue-grey', '300');
+ padding: 8px;
font-size: 13px;
- text-transform: uppercase;
- border-bottom: 1px dotted mc($primary, '400');
+ letter-spacing: 1px;
+ text-transform: uppercase;
+ box-shadow: 0 0 5px rgba(0,0,0,0.3);
}
.searchresults-list {
+ padding-bottom: 5px;
> li {
display: flex;
font-size: 14px;
- transition: background-color .3s linear;
+ transition: background-color .2s linear;
&:nth-child(odd) {
- background-color: mc($primary, '600');
+ background-color: mc('blue-grey', '900');
}
&.is-active, &:hover {
- background-color: mc($primary, '400');
+ background-color: mc('blue-grey', '600');
color: #FFF;
- border-left: 5px solid mc($primary, '200');
}
a {
- color: mc($primary, '50');
+ color: mc('blue-grey', '50');
display: flex;
align-items: center;
height: 30px;
diff --git a/client/scss/layout/_header.scss b/client/scss/layout/_header.scss
index 83eab039..0a014dd5 100644
--- a/client/scss/layout/_header.scss
+++ b/client/scss/layout/_header.scss
@@ -24,7 +24,7 @@ #notifload {
@include spinner(mc('indigo', '100'),0.5s,24px);
}
- &.active {
+ &.is-active {
opacity: 1;
}
@@ -33,4 +33,4 @@ #notifload {
#search-input {
max-width: 300px;
width: 33vw;
-}
\ No newline at end of file
+}
diff --git a/fuse.js b/fuse.js
index 956d2e51..88a77b50 100644
--- a/fuse.js
+++ b/fuse.js
@@ -88,43 +88,66 @@ let globalTasks = Promise.mapSeries([
if (err.code === 'ENOENT') {
console.info(colors.white(' └── ') + colors.green('Copy MathJax dependencies to assets...'))
return fs.ensureDirAsync('./assets/js/mathjax').then(() => {
- return fs.copyAsync('./node_modules/mathjax', './assets/js/mathjax', { filter: (src, dest) => {
- let srcNormalized = src.replace(/\\/g, '/')
- let shouldCopy = false
- console.log(srcNormalized)
- _.forEach([
- '/node_modules/mathjax',
- '/node_modules/mathjax/jax',
- '/node_modules/mathjax/jax/input',
- '/node_modules/mathjax/jax/output'
- ], chk => {
- if (srcNormalized.endsWith(chk)) {
- shouldCopy = true
+ return fs.copyAsync('./node_modules/mathjax', './assets/js/mathjax', {
+ filter: (src, dest) => {
+ let srcNormalized = src.replace(/\\/g, '/')
+ let shouldCopy = false
+ console.info(colors.white(' ' + srcNormalized))
+ _.forEach([
+ '/node_modules/mathjax',
+ '/node_modules/mathjax/jax',
+ '/node_modules/mathjax/jax/input',
+ '/node_modules/mathjax/jax/output'
+ ], chk => {
+ if (srcNormalized.endsWith(chk)) {
+ shouldCopy = true
+ }
+ })
+ _.forEach([
+ '/node_modules/mathjax/extensions',
+ '/node_modules/mathjax/MathJax.js',
+ '/node_modules/mathjax/jax/element',
+ '/node_modules/mathjax/jax/input/MathML',
+ '/node_modules/mathjax/jax/input/TeX',
+ '/node_modules/mathjax/jax/output/SVG'
+ ], chk => {
+ if (srcNormalized.indexOf(chk) > 0) {
+ shouldCopy = true
+ }
+ })
+ if (shouldCopy && srcNormalized.indexOf('/fonts/') > 0 && srcNormalized.indexOf('/STIX-Web') <= 1) {
+ shouldCopy = false
}
- })
- _.forEach([
- '/node_modules/mathjax/extensions',
- '/node_modules/mathjax/MathJax.js',
- '/node_modules/mathjax/jax/element',
- '/node_modules/mathjax/jax/input/MathML',
- '/node_modules/mathjax/jax/input/TeX',
- '/node_modules/mathjax/jax/output/SVG'
- ], chk => {
- if (srcNormalized.indexOf(chk) > 0) {
- shouldCopy = true
- }
- })
- if (shouldCopy && srcNormalized.indexOf('/fonts/') > 0 && srcNormalized.indexOf('/STIX-Web') <= 1) {
- shouldCopy = false
+ return shouldCopy
}
- return shouldCopy
- }})
+ })
})
} else {
throw err
}
})
},
+ /**
+ * i18n
+ */
+ () => {
+ console.info(colors.white(' └── ') + colors.green('Copying i18n client files...'))
+ return fs.ensureDirAsync('./assets/js/i18n').then(() => {
+ return fs.readJsonAsync('./server/locales/en/browser.json').then(enContent => {
+ return fs.readdirAsync('./server/locales').then(langs => {
+ return Promise.map(langs, lang => {
+ console.info(colors.white(' ' + lang + '.json'))
+ let outputPath = path.join('./assets/js/i18n', lang + '.json')
+ return fs.readJsonAsync(path.join('./server/locales', lang + '.json'), 'utf8').then((content) => {
+ return fs.outputJsonAsync(outputPath, _.defaultsDeep(content, enContent))
+ }).catch(err => { // eslint-disable-line handle-callback-err
+ return fs.outputJsonAsync(outputPath, enContent)
+ })
+ })
+ })
+ })
+ })
+ },
/**
* Bundle pre-init scripts
*/
@@ -144,6 +167,7 @@ let globalTasks = Promise.mapSeries([
* Delete Fusebox cache
*/
() => {
+ console.info(colors.white(' └── ') + colors.green('Clearing fuse-box cache...'))
return fs.emptyDirAsync('./.fusebox')
}
], f => { return f() })
@@ -156,7 +180,7 @@ const ALIASES = {
'brace-ext-modelist': 'brace/ext/modelist.js',
'simplemde': 'simplemde/dist/simplemde.min.js',
'socket.io-client': 'socket.io-client/dist/socket.io.js',
- 'vue': 'vue/dist/vue.min.js'
+ 'vue': (dev) ? 'vue/dist/vue.js' : 'vue/dist/vue.min.js'
}
const SHIMS = {
_preinit: {
@@ -182,7 +206,7 @@ globalTasks.then(() => {
plugins: [
fsbx.EnvPlugin({ NODE_ENV: (dev) ? 'development' : 'production' }),
fsbx.VuePlugin(),
- [ '.scss', fsbx.SassPlugin({ outputStyle: (dev) ? 'nested' : 'compressed' }), fsbx.CSSPlugin() ],
+ ['.scss', fsbx.SassPlugin({ outputStyle: (dev) ? 'nested' : 'compressed' }), fsbx.CSSPlugin()],
fsbx.BabelPlugin({ comments: false, presets: ['es2015'] }),
fsbx.JSONPlugin(),
!dev && fsbx.UglifyJSPlugin({
diff --git a/package.json b/package.json
index 8b328ec4..17503c36 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,7 @@
"fs-extra": "^3.0.1",
"git-wrapper2-promise": "^0.2.9",
"highlight.js": "^9.11.0",
- "i18next": "^8.2.0",
+ "i18next": "^8.3.0",
"i18next-express-middleware": "^1.0.5",
"i18next-node-fs-backend": "^1.0.0",
"image-size": "^0.5.4",
@@ -78,7 +78,7 @@
"markdown-it": "^8.3.1",
"markdown-it-abbr": "^1.0.4",
"markdown-it-anchor": "^4.0.0",
- "markdown-it-attrs": "^0.8.0",
+ "markdown-it-attrs": "^0.9.0",
"markdown-it-emoji": "^1.3.0",
"markdown-it-expand-tabs": "^1.0.12",
"markdown-it-external-links": "0.0.6",
@@ -126,11 +126,13 @@
},
"devDependencies": {
"@glimpse/glimpse": "^0.20.9",
+ "@panter/vue-i18next": "^0.4.1",
"babel-cli": "latest",
"babel-jest": "latest",
"babel-preset-es2015": "latest",
"brace": "^0.10.0",
"colors": "^1.1.2",
+ "consolidate": "^0.14.5",
"eslint": "latest",
"eslint-config-standard": "latest",
"eslint-plugin-import": "latest",
@@ -138,6 +140,7 @@
"eslint-plugin-promise": "latest",
"eslint-plugin-standard": "latest",
"fuse-box": "^2.0.0",
+ "i18next-xhr-backend": "^1.4.1",
"jest": "latest",
"jquery": "^3.2.1",
"jquery-contextmenu": "^2.4.5",
@@ -155,7 +158,8 @@
"vee-validate": "^2.0.0-rc.3",
"vue": "^2.3.3",
"vue-template-compiler": "^2.3.3",
- "vue-template-es2015-compiler": "^1.5.2"
+ "vue-template-es2015-compiler": "^1.5.2",
+ "vuex": "^2.3.1"
},
"jest": {
"collectCoverage": false,
@@ -166,4 +170,4 @@
"verbose": true
},
"snyk": true
-}
\ No newline at end of file
+}
diff --git a/server/controllers/admin.js b/server/controllers/admin.js
index 4514257d..09295faf 100644
--- a/server/controllers/admin.js
+++ b/server/controllers/admin.js
@@ -255,4 +255,11 @@ router.post('/settings/install', (req, res) => {
res.status(400).send('Sorry, Upgrade/Re-Install via the web UI is not yet ready. You must use the npm upgrade method in the meantime.').end()
})
+router.get('/theme', (req, res) => {
+ if (!res.locals.rights.manage) {
+ return res.render('error-forbidden')
+ }
+ res.render('pages/admin/theme', { adminTab: 'theme' })
+})
+
module.exports = router
diff --git a/server/locales/en/admin.json b/server/locales/en/admin.json
index 7a46d992..e62976fb 100644
--- a/server/locales/en/admin.json
+++ b/server/locales/en/admin.json
@@ -48,4 +48,4 @@
"edituser": "Edit User",
"uniqueid": "Unique ID"
}
-}
\ No newline at end of file
+}
diff --git a/server/locales/en/browser.json b/server/locales/en/browser.json
new file mode 100644
index 00000000..9ef3b278
--- /dev/null
+++ b/server/locales/en/browser.json
@@ -0,0 +1,16 @@
+{
+ "profile": {
+ "displayname": "Display Name",
+ "displaynameexample": "John Smith",
+ "email": "Email",
+ "password": "Password",
+ "passwordverify": "Verify Password",
+ "savechanges": "Save Changes"
+ },
+ "search": {
+ "placeholder": "Search...",
+ "results": "Search Results",
+ "nomatch": "No results matching your query",
+ "didyoumean": "Did you mean...?"
+ }
+}
diff --git a/server/locales/en/common.json b/server/locales/en/common.json
index 5461985c..40703a40 100644
--- a/server/locales/en/common.json
+++ b/server/locales/en/common.json
@@ -9,12 +9,6 @@
"home": "Home",
"top": "Return to top"
},
- "search": {
- "placeholder": "Search...",
- "results": "Search Results",
- "nomatch": "No results matching your query",
- "didyoumean": "Did you mean...?"
- },
"sidebar": {
"nav": "NAV",
"navigation": "Navigation",
@@ -24,9 +18,11 @@
"nav": {
"home": "Home",
"account": "Account",
+ "settings": "Settings",
"myprofile": "My Profile",
"stats": "Stats",
"syssettings": "System Settings",
+ "theme": "Color Theme",
"users": "Users",
"logout": "Logout",
"create": "Create",
@@ -51,4 +47,4 @@
"source": "Loading source...",
"editor": "Loading editor..."
}
-}
\ No newline at end of file
+}
diff --git a/server/locales/fr/common.json b/server/locales/fr/common.json
index f3a0a455..f8a809e3 100644
--- a/server/locales/fr/common.json
+++ b/server/locales/fr/common.json
@@ -24,9 +24,11 @@
"nav": {
"home": "Accueil",
"account": "Compte",
+ "settings": "Paramètres",
"myprofile": "Mon Profil",
"stats": "Statistiques",
"syssettings": "Paramètres système",
+ "theme": "Thème de couleur",
"users": "Utilisateurs",
"logout": "Se Déconnecter",
"create": "Créer",
@@ -51,4 +53,4 @@
"source": "Chargement de la source...",
"editor": "Chargement de l'éditeur"
}
-}
\ No newline at end of file
+}
diff --git a/server/views/common/header.pug b/server/views/common/header.pug
index 79451f64..cff22a80 100644
--- a/server/views/common/header.pug
+++ b/server/views/common/header.pug
@@ -9,26 +9,11 @@
= appconfig.title
.nav-center
block rootNavCenter
- .nav-item
- p.control(v-bind:class='{ "is-loading": searchload > 0 }')
- input.input#search-input(type='text', v-model='searchq', @keyup.esc='closeSearch', @keyup.down='moveDownSearch', @keyup.up='moveUpSearch', @keyup.enter='moveSelectSearch', debounce='400', placeholder=t('search.placeholder'))
+ search
span.nav-toggle
span
span
span
.nav-right
block rootNavRight
- i.nav-item#notifload
-
- transition(name='searchresults-anim', enter-active-class='slideInDown', leave-active-class='fadeOutUp')
- .searchresults.animated(v-show='searchactive', v-cloak, style={'display':'none'})
- p.searchresults-label= t('search.results')
- ul.searchresults-list
- li(v-if='searchres.length === 0')
- a: em= t('search.nomatch')
- li(v-for='sres in searchres', v-bind:class='{ "is-active": searchmovekey === "res." + sres.entryPath }')
- a(v-bind:href='"/" + sres.entryPath') {{ sres.title }}
- p.searchresults-label(v-if='searchsuggest.length > 0')= t('search.didyoumean')
- ul.searchresults-list(v-if='searchsuggest.length > 0')
- li(v-for='sug in searchsuggest', v-bind:class='{ "is-active": searchmovekey === "sug." + sug }')
- a(v-on:click='useSuggestion(sug)') {{ sug }}
+ loading-spinner
diff --git a/server/views/layout.pug b/server/views/layout.pug
index c1e5a970..66bc8dcc 100644
--- a/server/views/layout.pug
+++ b/server/views/layout.pug
@@ -9,7 +9,7 @@ html
meta(name='msapplication-TileImage', content='/favicons/ms-icon-144x144.png')
title= appconfig.title
- // Favicon
+ //- Favicon
each favsize in [57, 60, 72, 76, 114, 120, 144, 152, 180]
link(rel='apple-touch-icon', sizes=favsize + 'x' + favsize, href='/favicons/apple-icon-' + favsize + 'x' + favsize + '.png')
link(rel='icon', type='image/png', sizes='192x192', href='/favicons/android-icon-192x192.png')
@@ -17,7 +17,10 @@ html
link(rel='icon', type='image/png', sizes=favsize + 'x' + favsize, href='/favicons/favicon-' + favsize + 'x' + favsize + '.png')
link(rel='manifest', href='/manifest.json')
- // JS / CSS
+ //- Site Lang
+ script var siteLang = '!{appconfig.lang}';
+
+ //- JS / CSS
script(type='text/javascript', src='/js/libs.min.js')
script(type='text/javascript', src='/js/app.min.js')
@@ -26,7 +29,7 @@ html
body
#root.has-stickynav
include ./common/header.pug
- include ./common/alerts.pug
+ //-include ./common/alerts.pug
main
block content
include ./common/footer.pug
diff --git a/server/views/pages/admin/_layout.pug b/server/views/pages/admin/_layout.pug
index dfcac89b..5fd87c55 100644
--- a/server/views/pages/admin/_layout.pug
+++ b/server/views/pages/admin/_layout.pug
@@ -4,7 +4,7 @@ block rootNavCenter
h2.nav-item= t('nav.account')
block rootNavRight
- i.nav-item#notifload
+ loading-spinner
.nav-item
a.button.btn-edit-discard(href='/')
i.icon-home
@@ -48,6 +48,10 @@ block content
a(href='/admin/settings')
i.icon-cog
span= t('nav.syssettings')
+ li
+ a(href='/admin/theme')
+ i.icon-drop
+ span= t('nav.theme')
li
a(href='/logout')
i.icon-delete2
diff --git a/server/views/pages/admin/profile.pug b/server/views/pages/admin/profile.pug
index b32a5154..0ba46dcc 100644
--- a/server/views/pages/admin/profile.pug
+++ b/server/views/pages/admin/profile.pug
@@ -1,53 +1,51 @@
extends ./_layout.pug
block adminContent
- #page-type-admin-profile
- .hero
- h1.title#title= t('nav.myprofile')
- h2.subtitle= t('admin:profile.subtitle')
- .form-sections
- .columns.is-gapless
- .column.is-two-thirds
- section
- label.label= t('admin:profile.email')
- p.control.is-fullwidth
- input.input(type='text', placeholder=t('admin:profile.email'), value=user.email, disabled)
- if user.provider === 'local'
+ .hero
+ h1.title#title= t('nav.myprofile')
+ h2.subtitle= t('admin:profile.subtitle')
+ .form-sections
+ .columns.is-gapless
+ .column.is-two-thirds
+ admin-profile(inline-template, email=user.email, name=user.name, provider=user.provider)
+ div
section
- label.label= t('admin:profile.password')
+ label.label= t('admin:profile.email')
p.control.is-fullwidth
- input.input(type='password', placeholder=t('admin:profile.password'), value='********', v-model='password')
+ input.input(type='text', placeholder=t('admin:profile.email'), value=user.email, disabled)
+ if user.provider === 'local'
+ section
+ label.label= t('admin:profile.password')
+ p.control.is-fullwidth
+ input.input(type='password', placeholder=t('admin:profile.password'), value='********', v-model='password')
+ section
+ label.label= t('admin:profile.passwordverify')
+ p.control.is-fullwidth
+ input.input(type='password', placeholder=t('admin:profile.password'), value='********', v-model='passwordVerify')
section
- label.label= t('admin:profile.passwordverify')
+ label.label= t('admin:profile.displayname')
p.control.is-fullwidth
- input.input(type='password', placeholder=t('admin:profile.password'), value='********', v-model='passwordVerify')
- section
- label.label= t('admin:profile.displayname')
- p.control.is-fullwidth
- input.input(type='text', placeholder=t('admin:profile.displaynameexample'), v-model='name')
- section
- button.button.is-green(v-on:click='saveUser')
- i.icon-check
- span= t('admin:profile.savechanges')
- .column
- .panel-aside
- label.label= t('admin:profile.provider')
- p.control.account-profile-provider
- case user.provider
- when 'local': i.icon-server
- when 'windowslive': i.icon-windows2.is-blue
- when 'azure': i.icon-windows2.is-blue
- when 'google': i.icon-google.is-blue
- when 'facebook': i.icon-facebook.is-indigo
- when 'github': i.icon-github.is-grey
- when 'slack': i.icon-slack.is-purple
- when 'ldap': i.icon-arrow-repeat-outline
- default: i.icon-warning
- = t('auth:providers.' + user.provider)
- label.label= t('admin:profile.membersince')
- p.control= moment(user.createdAt).format('LL')
- label.label= t('admin:profile.lastprofileupdate')
- p.control= moment(user.updatedAt).format('LL')
-
- script(type='text/javascript').
- var usrDataName = "!{user.name}";
+ input.input(type='text', placeholder=t('admin:profile.displaynameexample'), v-model='name')
+ section
+ button.button.is-green(v-on:click='saveUser')
+ i.icon-check
+ span= t('admin:profile.savechanges')
+ .column
+ .panel-aside
+ label.label= t('admin:profile.provider')
+ p.control.account-profile-provider
+ case user.provider
+ when 'local': i.icon-server
+ when 'windowslive': i.icon-windows2.is-blue
+ when 'azure': i.icon-windows2.is-blue
+ when 'google': i.icon-google.is-blue
+ when 'facebook': i.icon-facebook.is-indigo
+ when 'github': i.icon-github.is-grey
+ when 'slack': i.icon-slack.is-purple
+ when 'ldap': i.icon-arrow-repeat-outline
+ default: i.icon-warning
+ = t('auth:providers.' + user.provider)
+ label.label= t('admin:profile.membersince')
+ p.control= moment(user.createdAt).format('LL')
+ label.label= t('admin:profile.lastprofileupdate')
+ p.control= moment(user.updatedAt).format('LL')
diff --git a/server/views/pages/admin/settings.pug b/server/views/pages/admin/settings.pug
index 29601be8..79d9926c 100644
--- a/server/views/pages/admin/settings.pug
+++ b/server/views/pages/admin/settings.pug
@@ -1,10 +1,10 @@
extends ./_layout.pug
block adminContent
- #page-type-admin-settings
- .hero
- h1.title#title= t('nav.syssettings')
- h2.subtitle= t('admin:settings.subtitle')
+ .hero
+ h1.title#title= t('nav.syssettings')
+ h2.subtitle= t('admin:settings.subtitle')
+ admin-settings(inline-template)
.form-sections
section
img(src='/images/logo.png', style={width:'200px', float:'right'})
@@ -34,4 +34,4 @@ block adminContent
p.is-small= t('admin:settings.flushsessionstext')
p: button.button.is-teal.is-outlined(v-on:click='flushsessions')= t('admin:settings.flushsessionsbtn')
- include ../../modals/admin-upgrade.pug
+ include ../../modals/admin-upgrade.pug
diff --git a/server/views/pages/admin/theme.pug b/server/views/pages/admin/theme.pug
new file mode 100644
index 00000000..301f9d8d
--- /dev/null
+++ b/server/views/pages/admin/theme.pug
@@ -0,0 +1,11 @@
+extends ./_layout.pug
+
+block adminContent
+ #page-type-admin-settings
+ .hero
+ h1.title#title= t('nav.theme')
+ h2.subtitle= t('admin:theme.subtitle')
+ .form-sections
+ section
+ label.label= t('admin:theme.primarycolor')
+ color-picker
diff --git a/server/views/pages/view.pug b/server/views/pages/view.pug
index fb4a8e21..f4a63c77 100644
--- a/server/views/pages/view.pug
+++ b/server/views/pages/view.pug
@@ -57,8 +57,8 @@ block content
if !isGuest
li
a(href='/admin')
- i.icon-head
- span= t('nav.account')
+ i.icon-cog
+ span= t('nav.settings')
else
li
a(href='/login')
diff --git a/yarn.lock b/yarn.lock
index c0886a64..fc32004a 100644
Binary files a/yarn.lock and b/yarn.lock differ