ソースを参照

feat: profile page to vue 3 composition + add fonts / width options to theme + site config preload

Nicolas Giard 3 年 前
コミット
c3bd7ff97e
41 ファイル変更623 行追加338 行削除
  1. 4 1
      server/db/migrations/3.0.0.js
  2. 26 26
      server/graph/resolvers/authentication.js
  3. 19 17
      server/graph/schemas/authentication.graphql
  4. 20 8
      server/graph/schemas/site.graphql
  5. 3 1
      server/models/sites.js
  6. 2 4
      ux/index.html
  7. 0 0
      ux/public/_assets/fonts/roboto-mono/roboto-mono.woff2
  8. BIN
      ux/public/_assets/fonts/roboto/roboto-all-300.woff2
  9. BIN
      ux/public/_assets/fonts/roboto/roboto-all-500.woff2
  10. BIN
      ux/public/_assets/fonts/roboto/roboto-all-700.woff2
  11. BIN
      ux/public/_assets/fonts/roboto/roboto-all-900.woff2
  12. BIN
      ux/public/_assets/fonts/roboto/roboto-all-regular.woff2
  13. 40 0
      ux/public/_assets/fonts/roboto/roboto.css
  14. BIN
      ux/public/_assets/fonts/rubik/rubik-variable-cyrillic-ext.woff2
  15. BIN
      ux/public/_assets/fonts/rubik/rubik-variable-cyrillic.woff2
  16. BIN
      ux/public/_assets/fonts/rubik/rubik-variable-hebrew.woff2
  17. BIN
      ux/public/_assets/fonts/rubik/rubik-variable-latin-ext.woff2
  18. BIN
      ux/public/_assets/fonts/rubik/rubik-variable-latin.woff2
  19. 39 0
      ux/public/_assets/fonts/rubik/rubik.css
  20. 1 0
      ux/public/_assets/icons/ultraviolet-fonts-app.svg
  21. 1 0
      ux/public/_assets/icons/ultraviolet-width.svg
  22. 2 3
      ux/quasar.config.js
  23. 41 8
      ux/src/App.vue
  24. 0 1
      ux/src/boot/i18n.js
  25. 30 23
      ux/src/components/HeaderNav.vue
  26. 23 11
      ux/src/components/PageNewMenu.vue
  27. 1 10
      ux/src/css/app.scss
  28. BIN
      ux/src/css/fonts/poppins-medium.woff2
  29. BIN
      ux/src/css/fonts/raleway-medium.woff2
  30. 0 4
      ux/src/css/quasar.variables.scss
  31. 12 4
      ux/src/i18n/locales/en.json
  32. 1 5
      ux/src/layouts/AdminLayout.vue
  33. 0 1
      ux/src/layouts/AuthLayout.vue
  34. 71 59
      ux/src/layouts/ProfileLayout.vue
  35. 1 1
      ux/src/pages/AdminGeneral.vue
  36. 102 6
      ux/src/pages/AdminTheme.vue
  37. 3 3
      ux/src/pages/Index.vue
  38. 52 51
      ux/src/pages/Login.vue
  39. 103 83
      ux/src/pages/Profile.vue
  40. 8 8
      ux/src/router/routes.js
  41. 18 0
      ux/src/stores/site.js

+ 4 - 1
server/db/migrations/3.0.0.js

@@ -486,10 +486,13 @@ exports.up = async knex => {
         injectCSS: '',
         injectHead: '',
         injectBody: '',
+        contentWidth: 'full',
         sidebarPosition: 'left',
         tocPosition: 'right',
         showSharingMenu: true,
-        showPrintBtn: true
+        showPrintBtn: true,
+        baseFont: 'roboto',
+        contentFont: 'roboto'
       }
     }
   })

+ 26 - 26
server/graph/resolvers/authentication.js

@@ -39,33 +39,33 @@ module.exports = {
           })
         }, []), 'key')
       }))
+    },
+    /**
+     * Fetch active authentication strategies
+     */
+    async authSiteStrategies (obj, args, context, info) {
+      let strategies = await WIKI.models.authentication.getStrategies()
+      strategies = strategies.map(stg => {
+        const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey]) || {}
+        return {
+          ...stg,
+          strategy: strategyInfo,
+          config: _.sortBy(_.transform(stg.config, (res, value, key) => {
+            const configData = _.get(strategyInfo.props, key, false)
+            if (configData) {
+              res.push({
+                key,
+                value: JSON.stringify({
+                  ...configData,
+                  value
+                })
+              })
+            }
+          }, []), 'key')
+        }
+      })
+      return args.enabledOnly ? _.filter(strategies, 'isEnabled') : strategies
     }
-    // /**
-    //  * Fetch active authentication strategies
-    //  */
-    // async activeStrategies (obj, args, context, info) {
-    //   let strategies = await WIKI.models.authentication.getStrategies()
-    //   strategies = strategies.map(stg => {
-    //     const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey]) || {}
-    //     return {
-    //       ...stg,
-    //       strategy: strategyInfo,
-    //       config: _.sortBy(_.transform(stg.config, (res, value, key) => {
-    //         const configData = _.get(strategyInfo.props, key, false)
-    //         if (configData) {
-    //           res.push({
-    //             key,
-    //             value: JSON.stringify({
-    //               ...configData,
-    //               value
-    //             })
-    //           })
-    //         }
-    //       }, []), 'key')
-    //     }
-    //   })
-    //   return args.enabledOnly ? _.filter(strategies, 'isEnabled') : strategies
-    // }
   },
   Mutation: {
     /**

+ 19 - 17
server/graph/schemas/authentication.graphql

@@ -7,10 +7,12 @@ extend type Query {
 
   apiState: Boolean
 
-  authStrategies(
-    siteId: UUID
+  authStrategies: [AuthenticationStrategy]
+
+  authSiteStrategies(
+    siteId: UUID!
     enabledOnly: Boolean
-  ): [AuthenticationStrategy]
+  ): [AuthenticationSiteStrategy]
 }
 
 extend type Mutation {
@@ -70,12 +72,12 @@ extend type Mutation {
 # -----------------------------------------------
 
 type AuthenticationStrategy {
-  key: String!
-  props: [KeyValuePair] @auth(requires: ["manage:system"])
-  title: String!
+  key: String
+  props: [KeyValuePair]
+  title: String
   description: String
   isAvailable: Boolean
-  useForm: Boolean!
+  useForm: Boolean
   usernameType: String
   logo: String
   color: String
@@ -83,16 +85,16 @@ type AuthenticationStrategy {
   icon: String
 }
 
-type AuthenticationActiveStrategy {
-  key: String!
-  strategy: AuthenticationStrategy!
-  displayName: String!
-  order: Int!
-  isEnabled: Boolean!
-  config: [KeyValuePair] @auth(requires: ["manage:system"])
-  selfRegistration: Boolean!
-  domainWhitelist: [String]! @auth(requires: ["manage:system"])
-  autoEnrollGroups: [Int]! @auth(requires: ["manage:system"])
+type AuthenticationSiteStrategy {
+  key: String
+  strategy: AuthenticationStrategy
+  displayName: String
+  order: Int
+  isEnabled: Boolean
+  config: [KeyValuePair]
+  selfRegistration: Boolean
+  domainWhitelist: [String]
+  autoEnrollGroups: [Int]
 }
 
 type AuthenticationLoginResponse {

+ 20 - 8
server/graph/schemas/site.graphql

@@ -3,42 +3,42 @@
 # ===============================================
 
 extend type Query {
-  sites: [Site] @auth(requires: ["manage:system"])
+  sites: [Site]
 
   siteById (
     id: UUID!
-  ): Site @auth(requires: ["manage:system"])
+  ): Site
 
   siteByHostname (
     hostname: String!
     exact: Boolean!
-  ): Site @auth(requires: ["manage:system"])
+  ): Site
 }
 
 extend type Mutation {
   createSite (
     hostname: String!
     title: String!
-  ): SiteCreateResponse @auth(requires: ["manage:system"])
+  ): SiteCreateResponse
 
   updateSite (
     id: UUID!
     patch: SiteUpdateInput!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 
   uploadSiteLogo (
     id: UUID!
     image: Upload!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 
   uploadSiteFavicon (
     id: UUID!
     image: Upload!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 
   deleteSite (
     id: UUID!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 }
 
 # -----------------------------------------------
@@ -103,13 +103,22 @@ type SiteTheme {
   injectCSS: String
   injectHead: String
   injectBody: String
+  contentWidth: SiteThemeWidth
   sidebarPosition: SiteThemePosition
   tocPosition: SiteThemePosition
   showSharingMenu: Boolean
   showPrintBtn: Boolean
+  baseFont: String
+  contentFont: String
+}
+
+enum SiteThemeWidth {
+  full
+  centered
 }
 
 enum SiteThemePosition {
+  off
   left
   right
 }
@@ -172,8 +181,11 @@ input SiteThemeInput {
   injectCSS: String
   injectHead: String
   injectBody: String
+  contentWidth: SiteThemeWidth
   sidebarPosition: SiteThemePosition
   tocPosition: SiteThemePosition
   showSharingMenu: Boolean
   showPrintBtn: Boolean
+  baseFont: String
+  contentFont: String
 }

+ 3 - 1
server/models/sites.js

@@ -97,7 +97,9 @@ module.exports = class Site extends Model {
           sidebarPosition: 'left',
           tocPosition: 'right',
           showSharingMenu: true,
-          showPrintBtn: true
+          showPrintBtn: true,
+          baseFont: 'roboto',
+          contentFont: 'roboto'
         }
       })
     })

+ 2 - 4
ux/index.html

@@ -8,9 +8,7 @@
     <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
     <title>Wiki.js</title>
     <!--preload-links-->
-    <link rel="preconnect" href="https://fonts.googleapis.com">
-    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-    <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300..900&display=swap" rel="stylesheet">
+    <link href="/_assets/fonts/roboto/roboto.css" rel="stylesheet" />
     <style type="text/css">
       @keyframes initspinner {
         to { transform: rotate(360deg); }
@@ -69,7 +67,7 @@
       </style>
     </noscript>
   </head>
-  <body>
+  <body class="wiki-root">
     <div class="init-loading"></div>
     <div id="app"><!-- quasar:entry-point --></div>
   </body>

+ 0 - 0
ux/src/css/fonts/roboto-mono.woff2 → ux/public/_assets/fonts/roboto-mono/roboto-mono.woff2


BIN
ux/public/_assets/fonts/roboto/roboto-all-300.woff2


BIN
ux/public/_assets/fonts/roboto/roboto-all-500.woff2


BIN
ux/public/_assets/fonts/roboto/roboto-all-700.woff2


BIN
ux/public/_assets/fonts/roboto/roboto-all-900.woff2


BIN
ux/public/_assets/fonts/roboto/roboto-all-regular.woff2


+ 40 - 0
ux/public/_assets/fonts/roboto/roboto.css

@@ -0,0 +1,40 @@
+/* roboto-300 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 300;
+  src: local(''),
+       url('/_assets/fonts/roboto/roboto-all-300.woff2') format('woff2')
+}
+/* roboto-regular - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 400;
+  src: local(''),
+       url('/_assets/fonts/roboto/roboto-all-regular.woff2') format('woff2')
+}
+/* roboto-500 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 500;
+  src: local(''),
+       url('/_assets/fonts/roboto/roboto-all-500.woff2') format('woff2')
+}
+/* roboto-700 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 700;
+  src: local(''),
+       url('/_assets/fonts/roboto/roboto-all-700.woff2') format('woff2')
+}
+/* roboto-900 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 900;
+  src: local(''),
+       url('/_assets/fonts/roboto/roboto-all-900.woff2') format('woff2')
+}

BIN
ux/public/_assets/fonts/rubik/rubik-variable-cyrillic-ext.woff2


BIN
ux/public/_assets/fonts/rubik/rubik-variable-cyrillic.woff2


BIN
ux/public/_assets/fonts/rubik/rubik-variable-hebrew.woff2


BIN
ux/public/_assets/fonts/rubik/rubik-variable-latin-ext.woff2


BIN
ux/public/_assets/fonts/rubik/rubik-variable-latin.woff2


+ 39 - 0
ux/public/_assets/fonts/rubik/rubik.css

@@ -0,0 +1,39 @@
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Rubik';
+  font-display: swap;
+  src: url(/_assets/fonts/rubik/rubik-variable-cyrillic-ext.woff2) format('woff2');
+  unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+  font-family: 'Rubik';
+  font-display: swap;
+  src: url(/_assets/fonts/rubik/rubik-variable-cyrillic.woff2) format('woff2');
+  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* hebrew */
+@font-face {
+  font-family: 'Rubik';
+  font-display: swap;
+  src: url(/_assets/fonts/rubik/rubik-variable-hebrew.woff2) format('woff2');
+  unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Rubik';
+  font-display: swap;
+  src: url(/_assets/fonts/rubik/rubik-variable-latin-ext.woff2) format('woff2');
+  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Rubik';
+  font-display: swap;
+  src: url(/_assets/fonts/rubik/rubik-variable-latin.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+body.wiki-root {
+  font-family: 'Rubik', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+}

+ 1 - 0
ux/public/_assets/icons/ultraviolet-fonts-app.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 40 40" width="80px" height="80px"><path fill="#b6dcfe" d="M37,29v-8c0,0,0-4-3-5c0,0,4-1,5,5v8H37z"/><path fill="#b6dcfe" d="M26,26c0,1.657,1.343,3,3,3v-6C27.343,23,26,24.343,26,26z"/><polygon fill="#b6dcfe" points="9,8 13,8 21,29.5 17,29.5"/><path fill="#4788c7" d="M21.469,29.325l-8-21.5C13.396,7.63,13.209,7.5,13,7.5H9c-0.001,0-0.003,0.001-0.004,0.001 C8.921,7.502,8.85,7.521,8.784,7.553C8.765,7.562,8.751,7.577,8.733,7.588C8.689,7.617,8.65,7.65,8.616,7.691 C8.609,7.701,8.597,7.705,8.59,7.715C8.581,7.728,8.581,7.744,8.573,7.757C8.562,7.778,8.544,7.794,8.535,7.816l-8.5,21.5 c-0.102,0.257,0.024,0.547,0.281,0.648C0.377,29.989,0.438,30,0.5,30c0.199,0,0.388-0.12,0.465-0.316L3.607,23H14 c0.016,0,0.029-0.008,0.044-0.009l2.487,6.684C16.604,29.87,16.791,30,17,30h4c0.164,0,0.317-0.08,0.41-0.215 C21.504,29.65,21.525,29.479,21.469,29.325z M4.003,22L8.986,9.396L13.676,22H4.003z M17.348,29L9.72,8.5h2.933L20.28,29H17.348z"/><path fill="#4788c7" d="M38.512,17.309c-1.107-1.354-3.235-2.852-7.082-2.304c-0.04,0.006-0.078,0.016-0.113,0.03 c-1.539,0.168-3.374,1.214-5.24,4.2c-0.146,0.233-0.075,0.542,0.159,0.688c0.232,0.146,0.542,0.076,0.688-0.159 c1.932-3.091,3.959-4.282,6.033-3.544c1.326,0.474,2.541,1.967,2.915,3.418C35.439,20.339,33.944,22,29.5,22 c-2.481,0-4.5,1.794-4.5,4s2.019,4,4.5,4c3.349,0,5.417-1.668,6.5-2.922V29.5c0,0.276,0.224,0.5,0.5,0.5h3 c0.276,0,0.5-0.224,0.5-0.5v-7.76C40,20.055,39.472,18.481,38.512,17.309z M26,26c0-1.509,1.306-2.761,3-2.97v5.939 C27.306,28.761,26,27.509,26,26z M30,28.976v-5.99c3.282-0.094,5.065-1.085,6-1.965v4.352C35.684,25.938,33.906,28.747,30,28.976z M39,29h-2v-8.5c0-1.604-0.979-3.368-2.329-4.439c1.248,0.284,2.274,0.912,3.067,1.881C38.552,18.937,39,20.285,39,21.74V29z"/></svg>

+ 1 - 0
ux/public/_assets/icons/ultraviolet-width.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 40 40" width="80px" height="80px"><path fill="#b6dcfe" d="M34.5 6.5H37.5V32.5H34.5z"/><path fill="#4788c7" d="M37,7v25h-2V7H37 M38,6h-4v27h4V6L38,6z"/><path fill="#b6dcfe" d="M1.5 6.5H4.5V32.5H1.5z"/><path fill="#4788c7" d="M4 7v25H2V7H4M5 6H1v27h4V6L5 6zM30.5 32A.5.5 0 1 0 30.5 33 .5.5 0 1 0 30.5 32zM28.5 32A.5.5 0 1 0 28.5 33 .5.5 0 1 0 28.5 32zM26.5 32A.5.5 0 1 0 26.5 33 .5.5 0 1 0 26.5 32zM24.5 32A.5.5 0 1 0 24.5 33 .5.5 0 1 0 24.5 32zM22.5 32A.5.5 0 1 0 22.5 33 .5.5 0 1 0 22.5 32zM20.5 32A.5.5 0 1 0 20.5 33 .5.5 0 1 0 20.5 32zM18.5 32A.5.5 0 1 0 18.5 33 .5.5 0 1 0 18.5 32zM16.5 32A.5.5 0 1 0 16.5 33 .5.5 0 1 0 16.5 32zM14.5 32A.5.5 0 1 0 14.5 33 .5.5 0 1 0 14.5 32zM12.5 32A.5.5 0 1 0 12.5 33 .5.5 0 1 0 12.5 32zM10.5 32A.5.5 0 1 0 10.5 33 .5.5 0 1 0 10.5 32zM8.5 32A.5.5 0 1 0 8.5 33 .5.5 0 1 0 8.5 32zM6.5 32A.5.5 0 1 0 6.5 33 .5.5 0 1 0 6.5 32zM30.5 6A.5.5 0 1 0 30.5 7 .5.5 0 1 0 30.5 6zM32.5 32A.5.5 0 1 0 32.5 33 .5.5 0 1 0 32.5 32zM32.5 6A.5.5 0 1 0 32.5 7 .5.5 0 1 0 32.5 6zM28.5 6A.5.5 0 1 0 28.5 7 .5.5 0 1 0 28.5 6zM26.5 6A.5.5 0 1 0 26.5 7 .5.5 0 1 0 26.5 6zM24.5 6A.5.5 0 1 0 24.5 7 .5.5 0 1 0 24.5 6zM22.5 6A.5.5 0 1 0 22.5 7 .5.5 0 1 0 22.5 6zM20.5 6A.5.5 0 1 0 20.5 7 .5.5 0 1 0 20.5 6zM18.5 6A.5.5 0 1 0 18.5 7 .5.5 0 1 0 18.5 6zM16.5 6A.5.5 0 1 0 16.5 7 .5.5 0 1 0 16.5 6zM14.5 6A.5.5 0 1 0 14.5 7 .5.5 0 1 0 14.5 6zM12.5 6A.5.5 0 1 0 12.5 7 .5.5 0 1 0 12.5 6zM10.5 6A.5.5 0 1 0 10.5 7 .5.5 0 1 0 10.5 6zM8.5 6A.5.5 0 1 0 8.5 7 .5.5 0 1 0 8.5 6zM6.5 6A.5.5 0 1 0 6.5 7 .5.5 0 1 0 6.5 6z"/><g><path fill="#4788c7" d="M12 21L20 21 20 18 12 18 12 13 5 19.5 12 26z"/></g><g><path fill="#4788c7" d="M27 18L19 18 19 21 27 21 27 26 34 19.5 27 13z"/></g></svg>

+ 2 - 3
ux/quasar.config.js

@@ -46,9 +46,8 @@ module.exports = configure(function (/* ctx */) {
       'fontawesome-v6',
       // 'eva-icons',
       // 'themify',
-      'line-awesome',
-      'roboto-font-latin-ext' // this or either 'roboto-font', NEVER both!
-
+      'line-awesome'
+      // 'roboto-font-latin-ext' // this or either 'roboto-font', NEVER both!
       // 'roboto-font', // optional, you are not bound to it
       // 'material-icons' // optional, you are not bound to it
     ],

+ 41 - 8
ux/src/App.vue

@@ -3,15 +3,42 @@ router-view
 </template>
 
 <script setup>
-import { nextTick, onMounted } from 'vue'
+import { nextTick, onMounted, reactive } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
 import { useSiteStore } from 'src/stores/site'
+import { setCssVar, useQuasar } from 'quasar'
 
 /* global siteConfig */
 
+// QUASAR
+
+const $q = useQuasar()
+
 // STORES
 
 const siteStore = useSiteStore()
 
+// ROUTER
+
+const router = useRouter()
+
+// STATE
+
+const state = reactive({
+  isInitialized: false
+})
+
+// THEME
+
+function applyTheme () {
+  $q.dark.set(siteStore.theme.dark)
+  setCssVar('primary', siteStore.theme.colorPrimary)
+  setCssVar('secondary', siteStore.theme.colorSecondary)
+  setCssVar('accent', siteStore.theme.colorAccent)
+  setCssVar('header', siteStore.theme.colorHeader)
+  setCssVar('sidebar', siteStore.theme.colorSidebar)
+}
+
 // INIT SITE STORE
 
 if (typeof siteConfig !== 'undefined') {
@@ -19,15 +46,21 @@ if (typeof siteConfig !== 'undefined') {
     id: siteConfig.id,
     title: siteConfig.title
   })
-} else {
-  siteStore.loadSite(window.location.hostname)
+  applyTheme()
 }
 
-// MOUNTED
-
-onMounted(async () => {
-  nextTick(() => {
+router.beforeEach(async (to, from) => {
+  if (!siteStore.id) {
+    console.info('No pre-cached site config. Loading site info...')
+    await siteStore.loadSite(window.location.hostname)
+    console.info(`Using Site ID ${siteStore.id}`)
+    applyTheme()
+  }
+})
+router.afterEach(() => {
+  if (!state.isInitialized) {
+    state.isInitialized = true
     document.querySelector('.init-loading').remove()
-  })
+  }
 })
 </script>

+ 0 - 1
ux/src/boot/i18n.js

@@ -6,7 +6,6 @@ export default boot(({ app }) => {
   const i18n = createI18n({
     legacy: false,
     locale: 'en-US',
-    allowComposition: true,
     messages
   })
 

+ 30 - 23
ux/src/components/HeaderNav.vue

@@ -17,14 +17,14 @@ q-header.bg-header.text-white.site-header(
           square
           )
           img(src='/_assets/logo-wikijs.svg')
-      q-toolbar-title.text-h6.font-poppins {{siteTitle}}
+      q-toolbar-title.text-h6 {{siteStore.title}}
     q-toolbar.gt-sm(
       style='height: 64px;'
       dark
       )
       q-input(
         dark
-        v-model='search'
+        v-model='state.search'
         standout='bg-white text-dark'
         dense
         rounded
@@ -37,8 +37,8 @@ q-header.bg-header.text-white.site-header(
         template(v-slot:append)
           q-icon.cursor-pointer(
             name='las la-times'
-            @click='search=``'
-            v-if='search.length > 0'
+            @click='state.search=``'
+            v-if='state.search.length > 0'
             :color='$q.dark.isActive ? `blue` : `grey-4`'
             )
       q-btn.q-ml-md(
@@ -47,7 +47,7 @@ q-header.bg-header.text-white.site-header(
         dense
         icon='las la-tags'
         color='grey'
-        to='/t'
+        to='/_tags'
         )
     q-toolbar(
       style='height: 64px;'
@@ -56,7 +56,7 @@ q-header.bg-header.text-white.site-header(
       q-space
       transition(name='syncing')
         q-spinner-rings.q-mr-sm(
-          v-show='isSyncing'
+          v-show='siteStore.routerLoading'
           color='orange'
           size='34px'
         )
@@ -83,26 +83,33 @@ q-header.bg-header.text-white.site-header(
       account-menu
 </template>
 
-<script>
-import { get } from 'vuex-pathify'
+<script setup>
 import AccountMenu from './AccountMenu.vue'
 import NewMenu from './PageNewMenu.vue'
 
-export default {
-  components: {
-    AccountMenu,
-    NewMenu
-  },
-  data () {
-    return {
-      search: ''
-    }
-  },
-  computed: {
-    isSyncing: get('isLoading', false),
-    siteTitle: get('site/title', false)
-  }
-}
+import { useI18n } from 'vue-i18n'
+import { useQuasar } from 'quasar'
+import { reactive } from 'vue'
+
+import { useSiteStore } from 'src/stores/site'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  search: ''
+})
 </script>
 
 <style lang="scss">

+ 23 - 11
ux/src/components/PageNewMenu.vue

@@ -24,20 +24,32 @@ q-menu(
       blueprint-icon(icon='advance')
       q-item-section.q-pr-sm New Redirection
     q-separator.q-my-sm(inset)
-    q-item(clickable, to='/f')
+    q-item(clickable, to='/_assets')
       blueprint-icon(icon='add-image')
       q-item-section.q-pr-sm Upload Media Asset
 </template>
 
-<script>
-export default {
-  data () {
-    return { }
-  },
-  methods: {
-    create (editor) {
-      this.$store.dispatch('page/pageCreate', { editor })
-    }
-  }
+<script setup>
+import { useI18n } from 'vue-i18n'
+import { useQuasar } from 'quasar'
+
+import { usePageStore } from 'src/stores/page'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const pageStore = usePageStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// METHODS
+
+function create (editor) {
+  pageStore.pageCreate({ editor })
 }
 </script>

+ 1 - 10
ux/src/css/app.scss

@@ -27,18 +27,9 @@ body::-webkit-scrollbar-thumb {
 // FONTS
 // ------------------------------------------------------------------
 
-@font-face {
-  font-family: 'Poppins';
-  src: url(./fonts/poppins-medium.woff2);
-}
-
-.font-poppins {
-  font-family: $font-poppins;
-}
-
 @font-face {
   font-family: 'Roboto Mono';
-  src: url(./fonts/roboto-mono.woff2);
+  src: url(/_assets/fonts/roboto-mono/roboto-mono.woff2);
 }
 
 .font-robotomono {

BIN
ux/src/css/fonts/poppins-medium.woff2


BIN
ux/src/css/fonts/raleway-medium.woff2


+ 0 - 4
ux/src/css/quasar.variables.scss

@@ -30,7 +30,3 @@ $dark-6: #070a0d;
 $dark-5: #0d1117;
 $dark-4: #161b22;
 $dark-3: #1e232a;
-
-// -- FONTS --
-
-$font-poppins: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;

+ 12 - 4
ux/src/i18n/locales/en.json

@@ -683,10 +683,10 @@
   "admin.theme.headHtmlInjection": "Head HTML Injection",
   "admin.theme.headHtmlInjectionHint": "HTML code to be injected just before the closing head tag. Usually for script tags.",
   "admin.theme.headerColor": "Header Color",
-  "admin.theme.headerColorHint": "The background color for the site top header.",
+  "admin.theme.headerColorHint": "The background color for the site top header. Does not apply to the administration area.",
   "admin.theme.iconset": "Icon Set",
   "admin.theme.iconsetHint": "Set of icons to use for the sidebar navigation.",
-  "admin.theme.layout": "Theme Layout",
+  "admin.theme.layout": "Layout",
   "admin.theme.options": "Theme Options",
   "admin.theme.primaryColor": "Primary Color",
   "admin.theme.primaryColorHint": "The main color for primary action buttons and most form elements.",
@@ -701,7 +701,7 @@
   "admin.theme.showSharingMenu": "Show Sharing Menu",
   "admin.theme.showSharingMenuHint": "Should the sharing menu be displayed on all pages.",
   "admin.theme.sidebarColor": "Sidebar Color",
-  "admin.theme.sidebarColorHint": "The background color for the side navigation menu on content pages.",
+  "admin.theme.sidebarColorHint": "The background color for the side navigation menu on content pages. Does not apply to the administration area.",
   "admin.theme.sidebarPosition": "Sidebar Position",
   "admin.theme.sidebarPositionHint": "On which side should the main site navigation sidebar be displayed.",
   "admin.theme.siteTheme": "Site Theme",
@@ -1429,5 +1429,13 @@
   "admin.general.footerExtraHint": "Optionally add more content to the footer, such as additional copyright terms or mandatory regulatory info.",
   "admin.general.urlHandling": "URL Handling",
   "admin.general.pageExtensions": "Page Extensions",
-  "admin.general.pageExtensionsHint": "A comma-separated list of URL extensions that will be treated as pages. For example, adding md will treat /foobar.md the same as /foobar."
+  "admin.general.pageExtensionsHint": "A comma-separated list of URL extensions that will be treated as pages. For example, adding md will treat /foobar.md the same as /foobar.",
+  "admin.theme.appearance": "Appearance",
+  "admin.theme.fonts": "Fonts",
+  "admin.theme.baseFont": "Base Font",
+  "admin.theme.baseFontHint": "The font used across the site for the interface.",
+  "admin.theme.contentFont": "Content Font",
+  "admin.theme.contentFontHint": "The font used specifically for page content.",
+  "admin.theme.contentWidth": "Content Width",
+  "admin.theme.contentWidthHint": "Should the content use all available viewport space or stay centered."
 }

+ 1 - 5
ux/src/layouts/AdminLayout.vue

@@ -6,7 +6,7 @@ q-layout.admin(view='hHh Lpr lff')
         q-btn(dense, flat, href='/')
           q-avatar(size='34px', square)
             img(src='/_assets/logo-wikijs.svg')
-        q-toolbar-title.text-h6.font-poppins Wiki.js
+        q-toolbar-title.text-h6 Wiki.js
       q-toolbar.gt-sm.justify-center(style='height: 64px;', dark)
         .text-overline.text-uppercase.text-grey {{t('admin.adminArea')}}
         q-badge.q-ml-sm(
@@ -267,10 +267,6 @@ watch(() => adminStore.currentSiteId, (newValue) => {
   }
 })
 
-setCssVar('primary', '#1976D2')
-setCssVar('secondary', '#02C39A')
-setCssVar('accent', '#f03a47')
-
 // MOUNTED
 
 onMounted(async () => {

+ 0 - 1
ux/src/layouts/AuthLayout.vue

@@ -12,7 +12,6 @@ q-layout(view='hHh lpr lff')
   .auth {
     background-color: #FFF;
     display: flex;
-    font-family: 'Rubik', sans-serif;
 
     @at-root .body--dark & {
       background-color: $dark-6;

+ 71 - 59
ux/src/layouts/ProfileLayout.vue

@@ -9,7 +9,7 @@ q-layout(view='hHh Lpr lff')
             v-for='navItem of sidenav'
             :key='navItem.key'
             clickable
-            :to='`/p/` + navItem.key'
+            :to='`/_profile/` + navItem.key'
             active-class='is-active'
             v-ripple
             )
@@ -21,7 +21,7 @@ q-layout(view='hHh Lpr lff')
           q-item(
             clickable
             v-ripple
-            to='/p/me'
+            to='/_profile/me'
             )
             q-item-section(side)
               q-icon(name='las la-id-card')
@@ -42,67 +42,79 @@ q-layout(view='hHh Lpr lff')
       span(style='font-size: 11px;') &copy; Cyberdyne Systems Corp. 2020 | Powered by #[strong Wiki.js]
 </template>
 
-<script>
+<script setup>
+import gql from 'graphql-tag'
+
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { onMounted, reactive, watch } from 'vue'
+
+import { useSiteStore } from 'src/stores/site'
+
 import HeaderNav from '../components/HeaderNav.vue'
 
-export default {
-  name: 'ProfileLayout',
-  components: {
-    HeaderNav
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const sidenav = [
+  {
+    key: 'info',
+    label: 'Profile',
+    icon: 'las la-user-circle'
   },
-  data () {
-    return {
-      sidenav: [
-        {
-          key: 'profile',
-          label: 'Profile',
-          icon: 'las la-user-circle'
-        },
-        {
-          key: 'avatar',
-          label: 'Avatar',
-          icon: 'las la-otter'
-        },
-        {
-          key: 'password',
-          label: 'Password',
-          icon: 'las la-key'
-        },
-        {
-          key: 'groups',
-          label: 'Groups',
-          icon: 'las la-users'
-        },
-        {
-          key: 'notifications',
-          label: 'Notifications',
-          icon: 'las la-bell'
-        },
-        {
-          key: 'pages',
-          label: 'My Pages',
-          icon: 'las la-file-alt'
-        },
-        {
-          key: 'activity',
-          label: 'Activity',
-          icon: 'las la-history'
-        }
-      ],
-      thumbStyle: {
-        right: '2px',
-        borderRadius: '5px',
-        backgroundColor: '#FFF',
-        width: '5px',
-        opacity: 0.5
-      },
-      barStyle: {
-        backgroundColor: '#000',
-        width: '9px',
-        opacity: 0.1
-      }
-    }
+  {
+    key: 'avatar',
+    label: 'Avatar',
+    icon: 'las la-otter'
+  },
+  {
+    key: 'password',
+    label: 'Password',
+    icon: 'las la-key'
+  },
+  {
+    key: 'groups',
+    label: 'Groups',
+    icon: 'las la-users'
+  },
+  {
+    key: 'notifications',
+    label: 'Notifications',
+    icon: 'las la-bell'
+  },
+  {
+    key: 'pages',
+    label: 'My Pages',
+    icon: 'las la-file-alt'
+  },
+  {
+    key: 'activity',
+    label: 'Activity',
+    icon: 'las la-history'
   }
+]
+const thumbStyle = {
+  right: '2px',
+  borderRadius: '5px',
+  backgroundColor: '#FFF',
+  width: '5px',
+  opacity: 0.5
+}
+const barStyle = {
+  backgroundColor: '#000',
+  width: '9px',
+  opacity: 0.1
 }
 </script>
 

+ 1 - 1
ux/src/pages/AdminGeneral.vue

@@ -267,7 +267,7 @@ q-page.admin-general
                   src='https://m.media-amazon.com/images/G/01/audibleweb/arya/navigation/audible_logo._V517446980_.svg'
                   style='height: 34px;'
                   )
-              q-toolbar-title.text-h6.font-poppins(v-if='state.config.logoText') {{state.config.title}}
+              q-toolbar-title.text-h6(v-if='state.config.logoText') {{state.config.title}}
         q-separator.q-my-sm(inset)
         q-item(tag='label')
           blueprint-icon(icon='information')

+ 102 - 6
ux/src/pages/AdminTheme.vue

@@ -38,7 +38,7 @@ q-page.admin-theme
       //- -----------------------
       q-card.shadow-1.q-pb-sm
         q-card-section.flex.items-center
-          .text-subtitle1 {{t('admin.theme.options')}}
+          .text-subtitle1 {{t('admin.theme.appearance')}}
           q-space
           q-btn.acrylic-btn(
             icon='las la-redo-alt'
@@ -93,6 +93,21 @@ q-page.admin-theme
       q-card.shadow-1.q-pb-sm.q-mt-md
         q-card-section
           .text-subtitle1 {{t('admin.theme.layout')}}
+        q-item
+          blueprint-icon(icon='width')
+          q-item-section
+            q-item-label {{t(`admin.theme.contentWidth`)}}
+            q-item-label(caption) {{t(`admin.theme.contentWidthHint`)}}
+          q-item-section.col-auto
+            q-btn-toggle(
+              v-model='state.config.contentWidth'
+              push
+              glossy
+              no-caps
+              toggle-color='primary'
+              :options='widthOptions'
+            )
+        q-separator.q-my-sm(inset)
         q-item
           blueprint-icon(icon='right-navigation-toolbar')
           q-item-section
@@ -153,9 +168,55 @@ q-page.admin-theme
 
     .col-6
       //- -----------------------
-      //- Code Injection
+      //- Fonts
       //- -----------------------
       q-card.shadow-1.q-pb-sm
+        q-card-section.flex.items-center
+          .text-subtitle1 {{t('admin.theme.fonts')}}
+          q-space
+          q-btn.acrylic-btn(
+            icon='las la-redo-alt'
+            :label='t(`admin.theme.resetDefaults`)'
+            flat
+            size='sm'
+            color='pink'
+            @click='resetFonts'
+          )
+        q-item
+          blueprint-icon(icon='fonts-app')
+          q-item-section
+            q-item-label {{t(`admin.theme.baseFont`)}}
+            q-item-label(caption) {{t(`admin.theme.baseFontHint`)}}
+          q-item-section
+            q-select(
+              outlined
+              v-model='state.config.baseFont'
+              :options='fonts'
+              emit-value
+              map-options
+              dense
+              :aria-label='t(`admin.theme.baseFont`)'
+              )
+        q-item
+          blueprint-icon(icon='fonts-app')
+          q-item-section
+            q-item-label {{t(`admin.theme.contentFont`)}}
+            q-item-label(caption) {{t(`admin.theme.contentFontHint`)}}
+          q-item-section
+            q-select(
+              outlined
+              v-model='state.config.contentFont'
+              :options='fonts'
+              emit-value
+              map-options
+              dense
+              :aria-label='t(`admin.theme.contentFont`)'
+              )
+
+      //- -----------------------
+      //- Code Injection
+      //- -----------------------
+      q-card.shadow-1.q-pb-sm.q-mt-md
         q-card-section
           .text-subtitle1 {{t('admin.theme.codeInjection')}}
         q-item
@@ -205,7 +266,7 @@ q-page.admin-theme
 import gql from 'graphql-tag'
 import { cloneDeep, startCase } from 'lodash-es'
 import { useI18n } from 'vue-i18n'
-import { useMeta, useQuasar } from 'quasar'
+import { setCssVar, useMeta, useQuasar } from 'quasar'
 import { onMounted, reactive, watch } from 'vue'
 
 import { useAdminStore } from 'src/stores/admin'
@@ -246,10 +307,13 @@ const state = reactive({
     colorAccent: '#f03a47',
     colorHeader: '#000',
     colorSidebar: '#1976D2',
+    contentWidth: 'full',
     sidebarPosition: 'left',
     tocPosition: 'right',
     showSharingMenu: true,
-    showPrintBtn: true
+    showPrintBtn: true,
+    baseFont: '',
+    contentFont: ''
   }
 })
 
@@ -261,11 +325,27 @@ const colorKeys = [
   'sidebar'
 ]
 
+const widthOptions = [
+  { label: 'Full Width', value: 'full' },
+  { label: 'Centered', value: 'centered' }
+]
+
 const rightLeftOptions = [
+  { label: 'Hide', value: 'off' },
   { label: 'Left', value: 'left' },
   { label: 'Right', value: 'right' }
 ]
 
+const fonts = [
+  { label: 'Inter', value: 'inter' },
+  { label: 'Open Sans', value: 'opensans' },
+  { label: 'Montserrat', value: 'montserrat' },
+  { label: 'Roboto', value: 'roboto' },
+  { label: 'Rubik', value: 'rubik' },
+  { label: 'Tajawal', value: 'tajawal' },
+  { label: 'User System Defaults', value: 'user' }
+]
+
 // WATCHERS
 
 watch(() => adminStore.currentSiteId, (newValue) => {
@@ -283,6 +363,11 @@ function resetColors () {
   state.config.colorSidebar = '#1976D2'
 }
 
+function resetFonts () {
+  state.config.baseFont = 'roboto'
+  state.config.contentFont = 'roboto'
+}
+
 async function load () {
   state.loading++
   $q.loading.show()
@@ -296,15 +381,18 @@ async function load () {
             id: $id
           ) {
             theme {
-              dark
+              baseFont
+              contentFont
               colorPrimary
               colorSecondary
               colorAccent
               colorHeader
               colorSidebar
+              dark
               injectCSS
               injectHead
               injectBody
+              contentWidth
               sidebarPosition
               tocPosition
               showSharingMenu
@@ -345,10 +433,13 @@ async function save () {
       injectCSS: state.config.injectCSS,
       injectHead: state.config.injectHead,
       injectBody: state.config.injectBody,
+      contentWidth: state.config.contentWidth,
       sidebarPosition: state.config.sidebarPosition,
       tocPosition: state.config.tocPosition,
       showSharingMenu: state.config.showSharingMenu,
-      showPrintBtn: state.config.showPrintBtn
+      showPrintBtn: state.config.showPrintBtn,
+      baseFont: state.config.baseFont,
+      contentFont: state.config.contentFont
     }
     const respRaw = await APOLLO_CLIENT.mutate({
       mutation: gql`
@@ -381,6 +472,11 @@ async function save () {
           theme: patchTheme
         })
         $q.dark.set(state.config.dark)
+        setCssVar('primary', state.config.colorPrimary)
+        setCssVar('secondary', state.config.colorSecondary)
+        setCssVar('accent', state.config.colorAccent)
+        setCssVar('header', state.config.colorHeader)
+        setCssVar('sidebar', state.config.colorSidebar)
       }
       $q.notify({
         type: 'positive',

+ 3 - 3
ux/src/pages/Index.vue

@@ -55,7 +55,7 @@ q-page.column
       q-input.no-height(
         borderless
         v-model='title'
-        input-class='font-poppins text-h4 text-grey-9'
+        input-class='text-h4 text-grey-9'
         input-style='padding: 0;'
         placeholder='Untitled Page'
         hide-hint
@@ -63,12 +63,12 @@ q-page.column
       q-input.no-height(
         borderless
         v-model='description'
-        input-class='font-poppins text-subtitle2 text-grey-7'
+        input-class='text-subtitle2 text-grey-7'
         input-style='padding: 0;'
         placeholder='Enter a short description'
         hide-hint
         )
-    .col.q-pa-md.font-poppins(v-else)
+    .col.q-pa-md(v-else)
       .text-h4.page-header-title {{title}}
       .text-subtitle2.page-header-subtitle {{description}}
 

+ 52 - 51
ux/src/pages/Login.vue

@@ -5,40 +5,40 @@
       img(src='/_assets/logo-wikijs.svg' :alt='siteStore.title')
     h2.auth-site-title {{ siteStore.title }}
     p.text-grey-7 Login to continue
-    template(v-if='state.strategies?.length > 1')
-    p Sign in with
-    .auth-strategies
-      q-btn(
-        label='GitHub'
-        icon='lab la-github'
-        push
-        no-caps
-        :color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
-        :text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
-        )
-      q-btn(
-        label='Google'
-        icon='lab la-google-plus'
-        push
-        no-caps
-        :color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
-        :text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
-        )
-      q-btn(
-        label='Twitter'
-        icon='lab la-twitter'
-        push
-        no-caps
-        :color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
-        :text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
-        )
-      q-btn(
-        label='Local'
-        icon='las la-seedling'
-        push
-        color='primary'
-        no-caps
-        )
+    template(v-if='state.strategies?.length > 1 || true')
+      p Sign in with
+      .auth-strategies
+        q-btn(
+          label='GitHub'
+          icon='lab la-github'
+          push
+          no-caps
+          :color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
+          :text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
+          )
+        q-btn(
+          label='Google'
+          icon='lab la-google-plus'
+          push
+          no-caps
+          :color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
+          :text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
+          )
+        q-btn(
+          label='Twitter'
+          icon='lab la-twitter'
+          push
+          no-caps
+          :color='$q.dark.isActive ? `blue-grey-9` : `grey-1`'
+          :text-color='$q.dark.isActive ? `white` : `blue-grey-9`'
+          )
+        q-btn(
+          label='Local'
+          icon='las la-seedling'
+          push
+          color='primary'
+          no-caps
+          )
     q-form.q-mt-md
       q-input(
         outlined
@@ -62,21 +62,22 @@
         no-caps
         icon='las la-sign-in-alt'
       )
-    q-separator.q-my-md
-    q-btn.acrylic-btn.full-width(
-      flat
-      color='primary'
-      label='Create an Account'
-      no-caps
-      icon='las la-user-plus'
-    )
-    q-btn.acrylic-btn.full-width.q-mt-sm(
-      flat
-      color='primary'
-      label='Forgot Password'
-      no-caps
-      icon='las la-life-ring'
-    )
+    template(v-if='true')
+      q-separator.q-my-md
+      q-btn.acrylic-btn.full-width(
+        flat
+        color='primary'
+        label='Create an Account'
+        no-caps
+        icon='las la-user-plus'
+      )
+      q-btn.acrylic-btn.full-width.q-mt-sm(
+        flat
+        color='primary'
+        label='Forgot Password'
+        no-caps
+        icon='las la-life-ring'
+      )
   .auth-bg(aria-hidden="true")
     img(src='https://docs.requarks.io/_assets/img/splash/1.jpg' alt='')
 </template>
@@ -189,9 +190,9 @@ async function fetchStrategies () {
   const resp = await APOLLO_CLIENT.query({
     query: gql`
       query loginFetchSiteStrategies(
-        $siteId: UUID
+        $siteId: UUID!
       ) {
-        authStrategies(
+        authSiteStrategies(
           siteId: $siteId
           enabledOnly: true
           ) {

+ 103 - 83
ux/src/pages/Profile.vue

@@ -1,190 +1,210 @@
 <template lang="pug">
 q-page.q-py-md(:style-fn='pageStyle')
-  .text-header {{$t('profile.myInfo')}}
+  .text-header {{t('profile.myInfo')}}
   q-item
     blueprint-icon(icon='contact')
     q-item-section
-      q-item-label {{$t(`profile.displayName`)}}
-      q-item-label(caption) {{$t(`profile.displayNameHint`)}}
+      q-item-label {{t(`profile.displayName`)}}
+      q-item-label(caption) {{t(`profile.displayNameHint`)}}
     q-item-section
       q-input(
         outlined
-        v-model='config.name'
+        v-model='state.config.name'
         dense
         hide-bottom-space
-        :aria-label='$t(`profile.displayName`)'
+        :aria-label='t(`profile.displayName`)'
         )
   q-separator.q-my-sm(inset)
   q-item
     blueprint-icon(icon='envelope')
     q-item-section
-      q-item-label {{$t(`profile.email`)}}
-      q-item-label(caption) {{$t(`profile.emailHint`)}}
+      q-item-label {{t(`profile.email`)}}
+      q-item-label(caption) {{t(`profile.emailHint`)}}
     q-item-section
       q-input(
         outlined
-        v-model='config.email'
+        v-model='state.config.email'
         dense
-        :aria-label='$t(`profile.email`)'
+        :aria-label='t(`profile.email`)'
         readonly
         )
   q-separator.q-my-sm(inset)
   q-item
     blueprint-icon(icon='address')
     q-item-section
-      q-item-label {{$t(`profile.location`)}}
-      q-item-label(caption) {{$t(`profile.locationHint`)}}
+      q-item-label {{t(`profile.location`)}}
+      q-item-label(caption) {{t(`profile.locationHint`)}}
     q-item-section
       q-input(
         outlined
-        v-model='config.location'
+        v-model='state.config.location'
         dense
         hide-bottom-space
-        :aria-label='$t(`profile.location`)'
+        :aria-label='t(`profile.location`)'
         )
   q-separator.q-my-sm(inset)
   q-item
     blueprint-icon(icon='new-job')
     q-item-section
-      q-item-label {{$t(`profile.jobTitle`)}}
-      q-item-label(caption) {{$t(`profile.jobTitleHint`)}}
+      q-item-label {{t(`profile.jobTitle`)}}
+      q-item-label(caption) {{t(`profile.jobTitleHint`)}}
     q-item-section
       q-input(
         outlined
-        v-model='config.jobTitle'
+        v-model='state.config.jobTitle'
         dense
         hide-bottom-space
-        :aria-label='$t(`profile.jobTitle`)'
+        :aria-label='t(`profile.jobTitle`)'
         )
   q-separator.q-my-sm(inset)
   q-item
     blueprint-icon(icon='gender')
     q-item-section
-      q-item-label {{$t(`profile.pronouns`)}}
-      q-item-label(caption) {{$t(`profile.pronounsHint`)}}
+      q-item-label {{t(`profile.pronouns`)}}
+      q-item-label(caption) {{t(`profile.pronounsHint`)}}
     q-item-section
       q-input(
         outlined
-        v-model='config.pronouns'
+        v-model='state.config.pronouns'
         dense
         hide-bottom-space
-        :aria-label='$t(`profile.pronouns`)'
+        :aria-label='t(`profile.pronouns`)'
         )
-  .text-header.q-mt-lg {{$t('profile.preferences')}}
+  .text-header.q-mt-lg {{t('profile.preferences')}}
   q-item
     blueprint-icon(icon='timezone')
     q-item-section
-      q-item-label {{$t(`profile.timezone`)}}
-      q-item-label(caption) {{$t(`profile.timezoneHint`)}}
+      q-item-label {{t(`profile.timezone`)}}
+      q-item-label(caption) {{t(`profile.timezoneHint`)}}
     q-item-section
       q-select(
         outlined
-        v-model='config.timezone'
-        :options='timezones'
+        v-model='state.config.timezone'
+        :options='dataStore.timezones'
         option-value='value'
         option-label='text'
         emit-value
         map-options
         dense
         options-dense
-        :aria-label='$t(`admin.general.defaultTimezone`)'
+        :aria-label='t(`admin.general.defaultTimezone`)'
       )
   q-separator.q-my-sm(inset)
   q-item
     blueprint-icon(icon='calendar')
     q-item-section
-      q-item-label {{$t(`profile.dateFormat`)}}
-      q-item-label(caption) {{$t(`profile.dateFormatHint`)}}
+      q-item-label {{t(`profile.dateFormat`)}}
+      q-item-label(caption) {{t(`profile.dateFormatHint`)}}
     q-item-section
       q-select(
         outlined
-        v-model='config.dateFormat'
+        v-model='state.config.dateFormat'
         emit-value
         map-options
         dense
-        :aria-label='$t(`admin.general.defaultDateFormat`)'
-        :options=`[
-          { label: $t('profile.localeDefault'), value: '' },
-          { label: 'DD/MM/YYYY', value: 'DD/MM/YYYY' },
-          { label: 'DD.MM.YYYY', value: 'DD.MM.YYYY' },
-          { label: 'MM/DD/YYYY', value: 'MM/DD/YYYY' },
-          { label: 'YYYY-MM-DD', value: 'YYYY-MM-DD' },
-          { label: 'YYYY/MM/DD', value: 'YYYY/MM/DD' }
-        ]`
+        :aria-label='t(`admin.general.defaultDateFormat`)'
+        :options='dateFormats'
       )
   q-separator.q-my-sm(inset)
   q-item
     blueprint-icon(icon='clock')
     q-item-section
-      q-item-label {{$t(`profile.timeFormat`)}}
-      q-item-label(caption) {{$t(`profile.timeFormatHint`)}}
+      q-item-label {{t(`profile.timeFormat`)}}
+      q-item-label(caption) {{t(`profile.timeFormatHint`)}}
     q-item-section.col-auto
       q-btn-toggle(
-        v-model='config.timeFormat'
+        v-model='state.config.timeFormat'
         push
         glossy
         no-caps
         toggle-color='primary'
-        :options=`[
-          { label: $t('admin.general.defaultTimeFormat12h'), value: '12h' },
-          { label: $t('admin.general.defaultTimeFormat24h'), value: '24h' }
-        ]`
+        :options='timeFormats'
       )
   q-separator.q-my-sm(inset)
   q-item(tag='label', v-ripple)
     blueprint-icon(icon='light-on')
     q-item-section
-      q-item-label {{$t(`profile.darkMode`)}}
-      q-item-label(caption) {{$t(`profile.darkModeHint`)}}
+      q-item-label {{t(`profile.darkMode`)}}
+      q-item-label(caption) {{t(`profile.darkModeHint`)}}
     q-item-section(avatar)
       q-toggle(
-        v-model='config.darkMode'
+        v-model='state.config.darkMode'
         color='primary'
         checked-icon='las la-check'
         unchecked-icon='las la-times'
-        :aria-label='$t(`profile.darkMode`)'
+        :aria-label='t(`profile.darkMode`)'
       )
   .actions-bar.q-mt-lg
     q-btn(
-      icon='mdi-check'
+      icon='las la-check'
       unelevated
       label='Save Changes'
       color='secondary'
     )
 </template>
 
-<script>
-import { get } from 'vuex-pathify'
-
-export default {
-  data () {
-    return {
-      config: {
-        name: 'John Doe',
-        email: 'john.doe@company.com',
-        location: '',
-        jobTitle: '',
-        pronouns: '',
-        dateFormat: '',
-        timeFormat: '12h',
-        darkMode: false
-      }
-    }
-  },
-  computed: {
-    timezones: get('data/timezones', false)
-  },
-  watch: {
-    'config.darkMode' (newValue) {
-      this.$q.dark.set(newValue)
-    }
-  },
-  methods: {
-    pageStyle (offset, height) {
-      return {
-        'min-height': `${height - 100 - offset}px`
-      }
-    }
+<script setup>
+import gql from 'graphql-tag'
+
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { onMounted, reactive, watch } from 'vue'
+
+import { useSiteStore } from 'src/stores/site'
+import { useDataStore } from 'src/stores/data'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const siteStore = useSiteStore()
+const dataStore = useDataStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('profile.title')
+})
+
+// DATA
+
+const state = reactive({
+  config: {
+    name: 'John Doe',
+    email: 'john.doe@company.com',
+    location: '',
+    jobTitle: '',
+    pronouns: '',
+    dateFormat: '',
+    timeFormat: '12h',
+    darkMode: false
+  }
+})
+
+const dateFormats = [
+  { value: '', label: t('profile.localeDefault') },
+  { value: 'DD/MM/YYYY', label: 'DD/MM/YYYY' },
+  { value: 'DD.MM.YYYY', label: 'DD.MM.YYYY' },
+  { value: 'MM/DD/YYYY', label: 'MM/DD/YYYY' },
+  { value: 'YYYY-MM-DD', label: 'YYYY-MM-DD' },
+  { value: 'YYYY/MM/DD', label: 'YYYY/MM/DD' }
+]
+const timeFormats = [
+  { value: '12h', label: t('admin.general.defaultTimeFormat12h') },
+  { value: '24h', label: t('admin.general.defaultTimeFormat24h') }
+]
+
+// METHODS
+
+function pageStyle (offset, height) {
+  return {
+    'min-height': `${height - 100 - offset}px`
   }
 }
 </script>

+ 8 - 8
ux/src/router/routes.js

@@ -15,14 +15,14 @@ const routes = [
       { path: '', component: () => import('../pages/Login.vue') }
     ]
   },
-  // {
-  //   path: '/p',
-  //   component: () => import('../layouts/ProfileLayout.vue'),
-  //   children: [
-  //     { path: '', redirect: '/p/profile' },
-  //     { path: 'profile', component: () => import('../pages/Profile.vue') }
-  //   ]
-  // },
+  {
+    path: '/_profile',
+    component: () => import('../layouts/ProfileLayout.vue'),
+    children: [
+      { path: '', redirect: '/_profile/info' },
+      { path: 'info', component: () => import('../pages/Profile.vue') }
+    ]
+  },
   {
     path: '/_admin',
     component: () => import('../layouts/AdminLayout.vue'),

+ 18 - 0
ux/src/stores/site.js

@@ -70,6 +70,20 @@ export const useSiteStore = defineStore('site', {
                 logoText
                 company
                 contentLicense
+                theme {
+                  dark
+                  colorPrimary
+                  colorSecondary
+                  colorAccent
+                  colorHeader
+                  colorSidebar
+                  sidebarPosition
+                  tocPosition
+                  showSharingMenu
+                  showPrintBtn
+                  baseFont
+                  contentFont
+                }
               }
             }
           `,
@@ -86,6 +100,10 @@ export const useSiteStore = defineStore('site', {
           this.logoUrl = clone(siteInfo.logoUrl)
           this.company = clone(siteInfo.company)
           this.contentLicense = clone(siteInfo.contentLicense)
+          this.theme = {
+            ...this.theme,
+            ...clone(siteInfo.theme)
+          }
         } else {
           throw new Error('Invalid Site')
         }