浏览代码

feat: content link states

Nick 5 年之前
父节点
当前提交
efab00fa0c

+ 1 - 0
client/components/common/nav-header.vue

@@ -71,6 +71,7 @@
                 single-line,
                 solo
                 flat
+                rounded
                 hide-details,
                 prepend-inner-icon='mdi-magnify',
                 :loading='searchIsLoading',

+ 72 - 8
client/components/tags.vue

@@ -21,6 +21,7 @@
           .overline.mr-3.animated.fadeInLeft Current Selection
           v-chip.mr-3.primary--text(
             v-for='tag of tagsSelected'
+            :key='`tagSelected-` + tag.tag'
             color='white'
             close
             @click:close='toggleTag(tag.tag)'
@@ -38,7 +39,7 @@
         template(v-else)
           v-icon.mr-3.animated.fadeInRight mdi-arrow-left
           .overline.animated.fadeInRight Select one or more tags
-      v-toolbar(color='grey lighten-4', flat, height='58')
+      v-toolbar(:color='$vuetify.theme.dark ? `grey darken-4-l5` : `grey lighten-4`', flat, height='58')
         v-text-field.tags-search(
           label='Search within results...'
           solo
@@ -50,12 +51,29 @@
           prepend-icon='mdi-file-document-box-search-outline'
           append-icon='mdi-arrow-right'
         )
+        template(v-if='locales.length > 1')
+          v-divider.mx-3(vertical)
+          .overline Locale
+          v-select.ml-2(
+            :items='locales'
+            v-model='locale'
+            :background-color='$vuetify.theme.dark ? `grey darken-3` : `white`'
+            hide-details
+            label='Locale'
+            item-text='name'
+            item-value='code'
+            rounded
+            single-line
+            dense
+            height='40'
+            style='max-width: 170px;'
+          )
         v-divider.mx-3(vertical)
         .overline Order By
         v-select.ml-2(
           :items='orderByItems'
           v-model='orderBy'
-          background-color='white'
+          :background-color='$vuetify.theme.dark ? `grey darken-3` : `white`'
           hide-details
           label='Order By'
           rounded
@@ -64,12 +82,13 @@
           height='40'
           style='max-width: 250px;'
         )
-        v-divider.mx-3(vertical)
-        v-btn-toggle(v-model='displayStyle', rounded, mandatory)
-          v-btn(text, height='40'): v-icon(small) mdi-view-list
-          v-btn(text, height='40'): v-icon(small) mdi-cards-variant
-          v-btn(text, height='40'): v-icon(small) mdi-format-align-justify
+        v-btn-toggle.ml-2(v-model='orderByDirection', rounded, mandatory)
+          v-btn(text, height='40'): v-icon(size='20') mdi-chevron-double-up
+          v-btn(text, height='40'): v-icon(size='20') mdi-chevron-double-down
       v-divider
+      .text-center.pt-10
+        img(src='/svg/icon-price-tag.svg')
+        .subtitle-2.grey--text Select one or more tags on the left.
     nav-footer
     notify
     search-results
@@ -77,16 +96,25 @@
 
 <script>
 import { get } from 'vuex-pathify'
+import VueRouter from 'vue-router'
 import _ from 'lodash'
 
 import tagsQuery from 'gql/common/common-pages-query-tags.gql'
 
+/* global siteLangs */
+
+const router = new VueRouter({
+  mode: 'history',
+  base: '/t'
+})
+
 export default {
   data() {
     return {
       tags: [],
       selection: [],
-      displayStyle: 0,
+      locale: 'any',
+      locales: [],
       orderBy: 'TITLE',
       orderByItems: [
         { text: 'Creation Date', value: 'CREATED' },
@@ -95,6 +123,7 @@ export default {
         { text: 'Path', value: 'PATH' },
         { text: 'Title', value: 'TITLE' }
       ],
+      orderByDirection: 0,
       scrollStyle: {
         vuescroll: {},
         scrollPanel: {
@@ -127,8 +156,27 @@ export default {
       return _.filter(this.tags, t => _.includes(this.selection, t.tag))
     }
   },
+  watch: {
+    locale (newValue, oldValue) {
+      this.rebuildURL()
+    },
+    orderBy (newValue, oldValue) {
+      this.rebuildURL()
+    },
+    orderByDirection (newValue, oldValue) {
+      this.rebuildURL()
+    }
+  },
+  router,
   created () {
     this.$store.commit('page/SET_MODE', 'tags')
+
+    this.locales = _.concat(
+      [{name: 'Any', code: 'any'}],
+      (siteLangs.length > 0 ? siteLangs : [])
+    )
+
+    this.selection = _.compact(this.$route.path.split('/'))
   },
   methods: {
     toggleTag (tag) {
@@ -137,9 +185,25 @@ export default {
       } else {
         this.selection.push(tag)
       }
+      this.rebuildURL()
     },
     isSelected (tag) {
       return _.includes(this.selection, tag)
+    },
+    rebuildURL () {
+      let urlObj = {
+        path: '/' + this.selection.join('/')
+      }
+      if (this.locale !== `any`) {
+        _.set(urlObj, 'query.lang', this.locale)
+      }
+      if (this.orderBy !== `TITLE`) {
+        _.set(urlObj, 'query.sort', this.orderBy.toLowerCase())
+      }
+      if (this.orderByDirection !== 0) {
+        _.set(urlObj, 'query.dir', this.orderByDirection === 0 ? `asc` : `desc`)
+      }
+      this.$router.push(urlObj)
     }
   },
   apollo: {

+ 1 - 0
client/static/svg/icon-price-tag.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="256" height="256"><path fill="#f7f7fb" d="M43.5,28L16.9,45.4c-5,2.3-2.3,9.1,0,14.1l17.9,17.9l62.2,23l17-47L43.5,28 M35.3,59.5 c-3.9-1.4-5.9-5.7-4.5-9.6c1.4-3.9,5.7-5.9,9.6-4.5s5.9,5.7,4.5,9.6C43.5,58.9,39.2,61,35.3,59.5"/><path fill="#dfdfe3" d="M30.353,52.448c0-0.846,0.144-1.706,0.446-2.548c1.098-3.057,3.977-4.946,7.048-4.946 c0.847,0,1.709,0.144,2.552,0.446l0,0c3.065,1.1,4.957,3.993,4.946,7.074C45.349,53.317,45.206,54.17,44.9,55l0,0 c-1.091,3.04-3.945,4.986-6.997,4.986c-0.864,0-1.743-0.156-2.603-0.486C32.242,58.402,30.352,55.521,30.353,52.448 M43.5,28 L16.9,45.4c-2.078,0.956-2.826,2.689-2.825,4.746c0,2.894,1.481,6.431,2.825,9.354l17.9,17.9l21.635,8H102.4l11.6-32L43.5,28"/><path fill="#454b54" d="M97,103.5c-0.4,0-0.7-0.1-1-0.2l-62.2-23c-0.4-0.2-0.8-0.4-1.1-0.7L14.8,61.7 c-0.6-0.6-0.9-1.3-0.9-2.1V45.4c0-1,0.5-2,1.4-2.5l26.6-17.5c0.8-0.5,1.8-0.6,2.7-0.3l70.5,25.5c0.7,0.3,1.4,0.8,1.7,1.5 s0.4,1.5,0.1,2.3l-17,47c-0.3,0.8-0.8,1.4-1.6,1.7C97.9,103.4,97.4,103.5,97,103.5z M36.5,74.9l58.7,21.7l15-41.4l-66.3-24L19.9,47 v11.3L36.5,74.9z"/><path fill="#6ec7b0" d="M34.8,27.5L16.9,45.4c-3.9,3.9-3.9,10.2,0,14.1l17.9,17.9h75v-50h-75V27.5z M37.8,60 c-4.1,0-7.5-3.4-7.5-7.5s3.4-7.5,7.5-7.5s7.5,3.4,7.5,7.5S42,60,37.8,60z"/><path fill="#454b54" d="M109.8,80.5h-75c-0.8,0-1.6-0.3-2.1-0.9L14.8,61.7C12.3,59.2,11,56,11,52.5s1.4-6.7,3.8-9.2 l17.9-17.9c0.6-0.6,1.3-0.9,2.1-0.9h75c1.7,0,3,1.3,3,3v50C112.8,79.2,111.5,80.5,109.8,80.5z M36.1,74.5h70.8v-44H36.1L19,47.5 c-1.3,1.3-2,3.1-2,5s0.7,3.6,2,4.9L36.1,74.5z"/><path fill="#fff" d="M89.8 50.5h-25c-1.7 0-3-1.3-3-3s1.3-3 3-3h25c1.7 0 3 1.3 3 3S91.5 50.5 89.8 50.5zM89.8 60.5h-25c-1.7 0-3-1.3-3-3s1.3-3 3-3h25c1.7 0 3 1.3 3 3S91.5 60.5 89.8 60.5z"/><path fill="#454b54" d="M7.8,85.5c-0.8,0-1.5-0.3-2.1-0.9c-1.2-1.2-1.2-3.1,0-4.2l9.1-9.1c1.2-1.2,3.1-1.2,4.2,0 c1.2,1.2,1.2,3.1,0,4.2l-9.1,9.1C9.4,85.2,8.6,85.5,7.8,85.5z"/></svg>

+ 32 - 3
client/themes/default/scss/app.scss

@@ -10,11 +10,40 @@
   }
 
   @at-root .theme--dark & {
-    // background-color: darken(mc('grey', '900'), 4%);
     color: mc('grey', '300');
+  }
+
+  // ---------------------------------
+  // LINKS
+  // ---------------------------------
 
-    a {
-      color: mc('blue', '100');
+  a {
+    color: mc('blue', '700');
+
+    &.is-internal-link.is-invalid-page {
+      color: mc('red', '700');
+
+      @at-root .theme--dark & {
+        color: mc('red', '200');
+      }
+    }
+
+    &.is-external-link {
+      padding-right: 3px;
+
+      &::after {
+        font-family: 'Material Design Icons';
+        font-size: 24px/1;
+        padding-left: 3px;
+        display: inline-block;
+        content: '\F3CC';
+        color: mc('grey', '500');
+        text-decoration: none;
+      }
+    }
+
+    @at-root .theme--dark & {
+      color: mc('blue', '200');
     }
   }
 

+ 1 - 1
server/graph/resolvers/contribute.js

@@ -22,7 +22,7 @@ module.exports = {
         name: _.get(c, 'name', 'Anonymous') || '',
         profile: _.get(c, 'profile', ''),
         tier: _.toLower(_.get(c, 'tier', 'backers')),
-        totalDonated: _.get(c, 'totalAmountDonated', 0),
+        totalDonated: Math.ceil(_.get(c, 'totalAmountDonated', 0)),
         twitter: _.get(c, 'twitter', '') || '',
         website: _.get(c, 'website', '') || ''
       }))

+ 2 - 0
server/jobs/render-page.js

@@ -8,6 +8,8 @@ module.exports = async (pageId) => {
 
   try {
     WIKI.models = require('../core/db').init()
+    await WIKI.configSvc.loadFromDb()
+    await WIKI.configSvc.applyFlags()
 
     const page = await WIKI.models.pages.getPageFromDb(pageId)
     if (!page) {

+ 94 - 0
server/modules/rendering/html-core/renderer.js

@@ -1,6 +1,8 @@
 const _ = require('lodash')
 const cheerio = require('cheerio')
 
+/* global WIKI */
+
 module.exports = {
   async render() {
     const $ = cheerio.load(this.input)
@@ -14,6 +16,98 @@ module.exports = {
       renderer.init($, child.config)
     }
 
+    // --------------------------------
+    // Detect internal / external links
+    // --------------------------------
+
+    let internalRefs = []
+    const reservedPrefixes = /^\/[a-z]\//gi
+
+    const isHostSet = WIKI.config.host.length > 7 && WIKI.config.host !== 'http://'
+    if (!isHostSet) {
+      WIKI.logger.warn('Host is not set. You must set the Site Host under General in the Administration Area!')
+    }
+
+    $('a').each((i, elm) => {
+      let href = $(elm).attr('href')
+
+      // -> Ignore empty links
+      if (!href || href.length < 1) {
+        return
+      }
+
+      // -> Strip host from local links
+      if (isHostSet && href.indexOf(WIKI.config.site.host) === 0) {
+        href = href.replace(WIKI.config.site.host, '')
+      }
+
+      // -> Assign local / external tag
+      if (href.indexOf('://') < 0) {
+        // -> Remove trailing slash
+        if (_.endsWith('/')) {
+          href = href.slice(0, -1)
+        }
+
+        // -> Check for system prefix
+        if (!reservedPrefixes.test(href)) {
+          $(elm).addClass(`is-internal-link`)
+
+          // -> Reformat paths
+          if (href.indexOf('/') !== 0) {
+            href = `/${this.page.localeCode}/${this.page.path}/${href}`
+          } else if (href.charAt(3) !== '/') {
+            href = `/${this.page.localeCode}${href}`
+          }
+
+          // -> Save internal references
+          internalRefs.push({
+            localeCode: href.substring(1, 3),
+            path: _.head(href.substring(4).split('#'))
+          })
+        } else {
+          $(elm).addClass(`is-system-link`)
+        }
+      } else {
+        $(elm).addClass(`is-external-link`)
+      }
+
+      // -> Update element
+      $(elm).attr('href', href)
+    })
+
+    // --------------------------------
+    // Detect internal link states
+    // --------------------------------
+
+    if (internalRefs.length > 0) {
+      // -> Find matching pages
+      const results = await WIKI.models.pages.query().column('path', 'localeCode').where(builder => {
+        internalRefs.forEach((ref, idx) => {
+          if (idx < 1) {
+            builder.where(ref)
+          } else {
+            builder.orWhere(ref)
+          }
+        })
+      })
+
+      // -> Apply tag to internal links for found pages
+      $('a.is-internal-link').each((i, elm) => {
+        const href = $(elm).attr('href')
+        const hrefObj = {
+          localeCode: href.substring(1, 3),
+          path: _.head(href.substring(4).split('#'))
+        }
+        if (_.some(results, r => {
+          return r.localeCode === hrefObj.localeCode && r.path === hrefObj.path
+        })) {
+          $(elm).addClass(`is-valid-page`)
+        } else {
+          $(elm).addClass(`is-invalid-page`)
+        }
+      })
+    }
+
     return $.html('body').replace('<body>', '').replace('</body>', '')
   }
 }