Add mobile support (Header, Drawer, ThumbList)
@ -26,6 +26,53 @@
 | 
			
		||||
      "multiple": false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "homepage.slogan": {
 | 
			
		||||
    "namespace": "homepage.slogan",
 | 
			
		||||
    "public": true,
 | 
			
		||||
    "title": "Slogan",
 | 
			
		||||
    "desc": "The slogan text (with typewriter effect), recommend 10-20 characters.",
 | 
			
		||||
    "type": "string",
 | 
			
		||||
    "default": "Hello World!"
 | 
			
		||||
  },
 | 
			
		||||
  "homepage.quote": {
 | 
			
		||||
    "namespace": "homepage.quote",
 | 
			
		||||
    "public": true,
 | 
			
		||||
    "title": "Quote",
 | 
			
		||||
    "desc": "The quote text (behinds the slogan).",
 | 
			
		||||
    "type": "longString",
 | 
			
		||||
    "default": "The most beautiful things in the world cannot be seen or even touched. \nThey must be felt with the heart."
 | 
			
		||||
  },
 | 
			
		||||
  "homepage.signature": {
 | 
			
		||||
    "namespace": "homepage.signature",
 | 
			
		||||
    "public": true,
 | 
			
		||||
    "title": "Signature",
 | 
			
		||||
    "desc": "The signature text (follows the quote).",
 | 
			
		||||
    "type": "string",
 | 
			
		||||
    "default": "—Helen Keller"
 | 
			
		||||
  },
 | 
			
		||||
  "homepage.cover.image": {
 | 
			
		||||
    "namespace": "homepage.cover.image",
 | 
			
		||||
    "public": true,
 | 
			
		||||
    "title": "Cover image",
 | 
			
		||||
    "desc": "Homepage cover image.",
 | 
			
		||||
    "type": "mediaPicker",
 | 
			
		||||
    "default": [
 | 
			
		||||
      {
 | 
			
		||||
        "id": 0,
 | 
			
		||||
        "url": "https://view.moezx.cc/images/2021/06/19/ca4748651c3c67e7e4c29c34fb13bc33.jpg"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "id": 0,
 | 
			
		||||
        "url": "https://view.moezx.cc/images/2021/07/21/c21fcdbf4cf09674537d928884863ecc.jpg"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "binds": {
 | 
			
		||||
      "title": "Select image for homepage cover.",
 | 
			
		||||
      "button": "Use this image",
 | 
			
		||||
      "type": "image",
 | 
			
		||||
      "multiple": true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "social.github": {
 | 
			
		||||
    "namespace": "social.github",
 | 
			
		||||
    "public": true,
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
define('SAKURA_VERSION', wp_get_theme()->get('Version'));
 | 
			
		||||
define('SAKURA_TEXT_DOMAIN', wp_get_theme()->get('TextDomain'));
 | 
			
		||||
 | 
			
		||||
define('SAKURA_DEVEPLOMENT', true);
 | 
			
		||||
define('SAKURA_DEVEPLOMENT', false);
 | 
			
		||||
define('SAKURA_DEVEPLOMENT_HOST', 'http://127.0.0.1:9000');
 | 
			
		||||
 | 
			
		||||
// PHP loaders
 | 
			
		||||
 | 
			
		||||
@ -67,7 +67,7 @@ class AdminPageHelper extends ViteHelper
 | 
			
		||||
 | 
			
		||||
    wp_localize_script('[type:module]chunk-entrance.js', 'InitState', (new InitStateController())->get_initial_state());
 | 
			
		||||
 | 
			
		||||
    wp_localize_script('[type:module]chunk-entrance.js', 'SakuraOptions', (new OptionController())->get_all_options());
 | 
			
		||||
    wp_localize_script('[type:module]chunk-entrance.js', 'SakuraOptions', ['data' => (new OptionController())->get_all_options()]);
 | 
			
		||||
 | 
			
		||||
    // <link rel="modulepreload" href="http://localhost:9000/assets/vendor.b3a324ba.js">
 | 
			
		||||
    foreach ($manifest[$entry_key]['imports'] as $index => $import) {
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
namespace Sakura\Helpers;
 | 
			
		||||
 | 
			
		||||
use Sakura\Controllers\OptionController;
 | 
			
		||||
// use Sakura\Controllers\OptionController;
 | 
			
		||||
 | 
			
		||||
class SetupHelper
 | 
			
		||||
{
 | 
			
		||||
@ -22,7 +22,8 @@ class SetupHelper
 | 
			
		||||
    // count post views
 | 
			
		||||
    add_action('get_header', [$this, 'set_post_views']);
 | 
			
		||||
    // Inite config options
 | 
			
		||||
    add_action('after_switch_theme', [new OptionController(), 'inite_theme'], 1, 2);
 | 
			
		||||
    // won't need anymore with options?
 | 
			
		||||
    // add_action('after_switch_theme', [new OptionController(), 'inite_theme'], 1, 2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public function setup()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								docs/dev.md
									
									
									
									
									
								
							
							
						
						@ -1,7 +1,17 @@
 | 
			
		||||
# Dev configurations
 | 
			
		||||
 | 
			
		||||
.env.development
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
```env
 | 
			
		||||
SSH_KEY_PATH='~/.ssh/id_rsa'
 | 
			
		||||
SSH_REMOTE_HOST='root@8.8.8.8'
 | 
			
		||||
SSH_REMOTE_WORK_DIR='/var/www/html/wp-contents/themes/sakura-next'
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
add this rewrite rule to Nginx:
 | 
			
		||||
 | 
			
		||||
```nginx
 | 
			
		||||
location /src/assets {
 | 
			
		||||
  rewrite ^/(.*)$ http://localhost:9000/$1 redirect;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										35
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@ -25,29 +25,29 @@
 | 
			
		||||
    "rsync:composer": "nodemon --watch './composer.json' --watch './composer.lock' scripts/rsync.mjs --composer",
 | 
			
		||||
    "icon": "yarn svgo -f ./src/assets/icons/ui/ && node scripts/import-svg-icons.mjs && eslint \"src/components/icon/**/*.{ts,js,json,vue}\" --fix && prettier \"src/components/icon/**/*.{ts,js,json,vue}\" --write",
 | 
			
		||||
    "mdc": "node scripts/mdc-upgrade.mjs",
 | 
			
		||||
    "options": "node scripts/options-export/copy-options.mjs && yarn tsc scripts/options-export/dump-options.ts && node scripts/options-export/dump-options.js"
 | 
			
		||||
    "options": "node scripts/options-export/copy-options.mjs && yarn tsc scripts/options-export/dump-options.ts && node scripts/options-export/dump-options.js && eslint \"src/admin/optionsType.ts\" --fix && prettier \"src/admin/optionsType.ts\" --write"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@formatjs/intl": "^1.13.2",
 | 
			
		||||
    "@material/button": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/card": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/checkbox": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/chips": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/dialog": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/elevation": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/form-field": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/list": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/menu": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/radio": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/ripple": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/switch": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/tab-bar": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/textfield": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/theme": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/typography": "^12.0.0-canary.9f68a932e.0",
 | 
			
		||||
    "@material/button": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/card": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/checkbox": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/chips": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/dialog": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/elevation": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/form-field": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/menu": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/radio": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/ripple": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/switch": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/tab-bar": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/textfield": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/theme": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@material/typography": "^12.0.0-canary.90e08fc6b.0",
 | 
			
		||||
    "@vueuse/core": "^5.1.3",
 | 
			
		||||
    "@yzfe/svgicon": "^1.0.1",
 | 
			
		||||
    "@yzfe/vue3-svgicon": "^1.0.1",
 | 
			
		||||
    "animate.css": "^4.1.1",
 | 
			
		||||
    "axios": "^0.21.1",
 | 
			
		||||
    "camelcase-keys": "^7.0.0",
 | 
			
		||||
    "chroma-js": "^2.1.2",
 | 
			
		||||
@ -58,6 +58,7 @@
 | 
			
		||||
    "lodash": "^4.17.21",
 | 
			
		||||
    "marked": "^2.1.3",
 | 
			
		||||
    "normalize.css": "^8.0.1",
 | 
			
		||||
    "perfect-scrollbar": "^1.5.2",
 | 
			
		||||
    "sass": "^1.35.1",
 | 
			
		||||
    "sass-loader": "^12.1.0",
 | 
			
		||||
    "snakecase-keys": "^4.0.2",
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,12 @@ const template = (importContent, dataContent) => `
 | 
			
		||||
<!-- # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -->
 | 
			
		||||
<!-- # Generated by scripts/import-svg-icons.js -->
 | 
			
		||||
<template>
 | 
			
		||||
  <svg-icon :data="svg[$props.name]" original></svg-icon>
 | 
			
		||||
  <svg-icon
 | 
			
		||||
    :data="svg[$props.name]"
 | 
			
		||||
    :width="$props.width"
 | 
			
		||||
    :height="$props.height"
 | 
			
		||||
    original
 | 
			
		||||
  ></svg-icon>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
  import { defineComponent } from 'vue'
 | 
			
		||||
@ -17,6 +22,8 @@ const template = (importContent, dataContent) => `
 | 
			
		||||
    name: 'Icon',
 | 
			
		||||
    props: {
 | 
			
		||||
      name: { type: String },
 | 
			
		||||
      width: { type: String, default: '100%' },
 | 
			
		||||
      height: { type: String, default: '100%' },
 | 
			
		||||
    },
 | 
			
		||||
    setup(){
 | 
			
		||||
      const svg = {
 | 
			
		||||
 | 
			
		||||
@ -3,13 +3,29 @@ import { writeFileSync } from 'fs'
 | 
			
		||||
 | 
			
		||||
const exportOptions: { [key: string]: any } = {}
 | 
			
		||||
 | 
			
		||||
const optionTypesTemplate = (fill: string) => {
 | 
			
		||||
  return `
 | 
			
		||||
    // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
 | 
			
		||||
    // Generated by scripts/options-export/dump-options.ts
 | 
			
		||||
    export interface SakuraOptions {
 | 
			
		||||
      ${fill}
 | 
			
		||||
    }
 | 
			
		||||
  `
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let types = ''
 | 
			
		||||
 | 
			
		||||
Object.keys(options).forEach((tab) => {
 | 
			
		||||
  options[tab].options.forEach((option) => {
 | 
			
		||||
    if (option.depends) delete option.depends // remove function
 | 
			
		||||
    exportOptions[option.namespace] = option
 | 
			
		||||
    types += `'${option.namespace}': any, `
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
console.dir(exportOptions)
 | 
			
		||||
 | 
			
		||||
const optionTypes = optionTypesTemplate(types)
 | 
			
		||||
 | 
			
		||||
writeFileSync('./app/configs/options.json', JSON.stringify(exportOptions, null, 2), { flag: 'w+' })
 | 
			
		||||
writeFileSync('./src/admin/optionsType.ts', optionTypes, { flag: 'w+' })
 | 
			
		||||
 | 
			
		||||
@ -21,8 +21,8 @@ export interface Options {
 | 
			
		||||
 | 
			
		||||
const options: Options = {
 | 
			
		||||
  basic: {
 | 
			
		||||
    title: 'Basic',
 | 
			
		||||
    desc: 'The basic options',
 | 
			
		||||
    title: intl.formatMessage({ id: 'options.basicTitle', defaultMessage: 'Basic' }),
 | 
			
		||||
    desc: intl.formatMessage({ id: 'options.basicDesc', defaultMessage: 'The basic options' }),
 | 
			
		||||
    icon: 'fas fa-address-card',
 | 
			
		||||
    options: [
 | 
			
		||||
      // basic.site.title
 | 
			
		||||
@ -69,6 +69,95 @@ const options: Options = {
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  homepage: {
 | 
			
		||||
    title: intl.formatMessage({ id: 'options.homepageTitle', defaultMessage: 'Homepage' }),
 | 
			
		||||
    desc: intl.formatMessage({ id: 'options.homepageDesc', defaultMessage: 'Homepage options' }),
 | 
			
		||||
    icon: 'fas fa-home',
 | 
			
		||||
    options: [
 | 
			
		||||
      // homepage.slogan
 | 
			
		||||
      {
 | 
			
		||||
        namespace: 'homepage.slogan',
 | 
			
		||||
        public: true,
 | 
			
		||||
        title: intl.formatMessage({
 | 
			
		||||
          id: 'options.homepage.slogan.title',
 | 
			
		||||
          defaultMessage: 'Slogan',
 | 
			
		||||
        }),
 | 
			
		||||
        desc: intl.formatMessage({
 | 
			
		||||
          id: 'options.homepage.slogan.desc',
 | 
			
		||||
          defaultMessage: 'The slogan text (with typewriter effect), recommend 10-20 characters.',
 | 
			
		||||
        }),
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        default: 'Hello World!',
 | 
			
		||||
      },
 | 
			
		||||
      // homepage.quote
 | 
			
		||||
      {
 | 
			
		||||
        namespace: 'homepage.quote',
 | 
			
		||||
        public: true,
 | 
			
		||||
        title: intl.formatMessage({
 | 
			
		||||
          id: 'options.homepage.quote.title',
 | 
			
		||||
          defaultMessage: 'Quote',
 | 
			
		||||
        }),
 | 
			
		||||
        desc: intl.formatMessage({
 | 
			
		||||
          id: 'options.homepage.signature.desc',
 | 
			
		||||
          defaultMessage: 'The quote text (behinds the slogan).',
 | 
			
		||||
        }),
 | 
			
		||||
        type: 'longString',
 | 
			
		||||
        default:
 | 
			
		||||
          'The most beautiful things in the world cannot be seen or even touched. \nThey must be felt with the heart.',
 | 
			
		||||
      },
 | 
			
		||||
      // homepage.signature
 | 
			
		||||
      {
 | 
			
		||||
        namespace: 'homepage.signature',
 | 
			
		||||
        public: true,
 | 
			
		||||
        title: intl.formatMessage({
 | 
			
		||||
          id: 'options.homepage.signature.title',
 | 
			
		||||
          defaultMessage: 'Signature',
 | 
			
		||||
        }),
 | 
			
		||||
        desc: intl.formatMessage({
 | 
			
		||||
          id: 'options.homepage.signature.desc',
 | 
			
		||||
          defaultMessage: 'The signature text (follows the quote).',
 | 
			
		||||
        }),
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        default: '—Helen Keller',
 | 
			
		||||
      },
 | 
			
		||||
      // homepage.cover.image
 | 
			
		||||
      {
 | 
			
		||||
        namespace: 'homepage.cover.image',
 | 
			
		||||
        public: true,
 | 
			
		||||
        title: intl.formatMessage({
 | 
			
		||||
          id: 'options.homepage.cover.image.title',
 | 
			
		||||
          defaultMessage: 'Cover images',
 | 
			
		||||
        }),
 | 
			
		||||
        desc: intl.formatMessage({
 | 
			
		||||
          id: 'options.homepage.cover.image.desc',
 | 
			
		||||
          defaultMessage: 'Homepage cover images. Will enable slide show with multiple selections.',
 | 
			
		||||
        }),
 | 
			
		||||
        type: 'mediaPicker',
 | 
			
		||||
        default: [
 | 
			
		||||
          {
 | 
			
		||||
            id: 0,
 | 
			
		||||
            url: 'https://view.moezx.cc/images/2021/06/19/ca4748651c3c67e7e4c29c34fb13bc33.jpg',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            id: 0,
 | 
			
		||||
            url: 'https://view.moezx.cc/images/2021/07/21/c21fcdbf4cf09674537d928884863ecc.jpg',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        binds: {
 | 
			
		||||
          title: intl.formatMessage({
 | 
			
		||||
            id: 'options.homepage.cover.image.binds.title',
 | 
			
		||||
            defaultMessage: 'Select image for homepage cover.',
 | 
			
		||||
          }),
 | 
			
		||||
          button: intl.formatMessage({
 | 
			
		||||
            id: 'options.homepage.cover.image.binds.button',
 | 
			
		||||
            defaultMessage: 'Use this image',
 | 
			
		||||
          }),
 | 
			
		||||
          type: 'image',
 | 
			
		||||
          multiple: true,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  social: {
 | 
			
		||||
    title: 'Social',
 | 
			
		||||
    icon: 'fas fa-users',
 | 
			
		||||
@ -171,7 +260,8 @@ const options: Options = {
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  thirdParty: {
 | 
			
		||||
    title: 'Third party services',
 | 
			
		||||
    title: 'Third party',
 | 
			
		||||
    desc: 'The third party services options',
 | 
			
		||||
    icon: 'fas fa-bezier-curve',
 | 
			
		||||
    options: [
 | 
			
		||||
      // thirdParty.reCaptcha.enable
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								src/admin/optionsType.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,27 @@
 | 
			
		||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
 | 
			
		||||
// Generated by scripts/options-export/dump-options.ts
 | 
			
		||||
export interface SakuraOptions {
 | 
			
		||||
  'basic.site.title': any
 | 
			
		||||
  'basic.site.logo': any
 | 
			
		||||
  'homepage.slogan': any
 | 
			
		||||
  'homepage.quote': any
 | 
			
		||||
  'homepage.signature': any
 | 
			
		||||
  'homepage.cover.image': any
 | 
			
		||||
  'social.github': any
 | 
			
		||||
  'social.gitlab': any
 | 
			
		||||
  'social.twitter': any
 | 
			
		||||
  'social.weibo': any
 | 
			
		||||
  'social.facebook': any
 | 
			
		||||
  'social.stackoverflow': any
 | 
			
		||||
  'thirdParty.reCaptcha.enable': any
 | 
			
		||||
  'thirdParty.reCaptcha.version': any
 | 
			
		||||
  'thirdParty.reCaptcha.siteKey': any
 | 
			
		||||
  'thirdParty.reCaptcha.secretKey': any
 | 
			
		||||
  'other.hello': any
 | 
			
		||||
  'demo.string': any
 | 
			
		||||
  'demo.longString': any
 | 
			
		||||
  'demo.switcher': any
 | 
			
		||||
  'demo.choose': any
 | 
			
		||||
  'demo.selection': any
 | 
			
		||||
  'demo.mediaPicker': any
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								src/assets/icons/ui/social.email.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,2 @@
 | 
			
		||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1626852417721" class="icon" viewBox="0 0 1385 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2021" xmlns:xlink="http://www.w3.org/1999/xlink" width="270.5078125" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }
 | 
			
		||||
</style></defs><path d="M1226.571294 36.442353h-1090.258823c-74.992941 0-136.312471 54.211765-136.312471 120.470588v722.82353c0 66.258824 61.319529 120.470588 136.312471 120.470588h1090.258823c74.992941 0 136.312471-54.211765 136.312471-120.470588v-722.82353c0-66.258824-61.319529-120.470588-136.312471-120.470588z m0 240.941176l-545.129412 301.176471-545.129411-301.176471v-120.470588l545.129411 301.176471 545.129412-301.176471v120.470588z" fill="#FFCB05" p-id="2022"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
							
								
								
									
										1
									
								
								src/assets/icons/ui/social.telegram.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1 @@
 | 
			
		||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M1024 512c0 281.6-230.4 512-512 512S0 793.6 0 512 230.4 0 512 0s512 230.4 512 512z" fill="#36AAE8"/><path d="m696.32 778.24 102.4-486.4c10.24-40.96-15.36-61.44-46.08-51.2l-604.16 230.4c-40.96 15.36-40.96 40.96-5.12 51.2l153.6 51.2 363.52-230.4c15.36-10.24 30.72-5.12 20.48 5.12L389.12 614.4l-10.24 158.72c15.36 0 25.6-5.12 30.72-15.36l76.8-71.68 158.72 117.76c25.6 15.36 46.08 5.12 51.2-25.6z" fill="#FFF"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 524 B  | 
@ -1 +1 @@
 | 
			
		||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M70.13 125.364h883.737v828.503H70.131z" fill="#1296db"/><path d="M513.159 1174.801c-268.49.663-510.524-161.06-613.258-408.949s-46.452-532.948 143.165-722.51S518.406-203.22 765.742-99.823a663.079 663.079 0 0 1 409.06 613.092c0 365.26-295.666 660.87-661.643 661.532zm145.816-956.479-20.547 2.651c-12.593 2.652-25.186 6.628-37.116 11.268a164.817 164.817 0 0 0-94.118 112.014c-6.628 25.849-6.628 53.024 0 78.21-25.187 0-50.373-3.314-74.234-9.942a487.712 487.712 0 0 1-202.873-98.757 305.718 305.718 0 0 1-49.103-48.385 135.267 135.267 0 0 1-15.244-17.896 185.419 185.419 0 0 0-17.896 43.083 172.66 172.66 0 0 0 37.117 151.781c8.617 10.605 21.873 17.233 30.489 26.513a43.8 43.8 0 0 1-22.535-1.989 163.16 163.16 0 0 1-35.129-9.942l-17.895-6.628a159.956 159.956 0 0 0 78.21 143.165c15.907 11.93 34.466 19.222 54.35 21.873-10.605 9.28-57.664 5.302-74.234 3.314 16.57 50.373 55.676 89.478 105.441 106.711 15.907 5.965 32.477 8.616 49.047 8.616a218.172 218.172 0 0 1-45.07 29.164 358.797 358.797 0 0 1-99.42 36.454c-15.245 3.314-31.152 2.651-48.385 5.302a236.952 236.952 0 0 1-53.024-1.988l14.581 8.616c15.245 9.28 30.49 17.233 47.06 23.861 31.814 13.256 64.291 23.861 97.431 31.815 78.21 16.57 159.79 13.919 236.676-7.954 173.71-54.35 283.735-181.608 325.546-369.18 6.628-35.13 9.28-71.583 7.954-107.375l26.512-21.872a278.1 278.1 0 0 0 56.338-64.955 330.849 330.849 0 0 1-94.118 25.85c6.628-3.315 12.594-7.954 18.559-13.257 25.186-20.547 43.745-47.721 53.687-78.21l-20.547 11.267a269.208 269.208 0 0 1-86.164 28.5 167.358 167.358 0 0 0-130.628-52.36v.662h-.718z" fill="#fff"/></svg>
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" style="enable-background:new 0 0 400 400" xml:space="preserve"><circle cx="200" cy="200" r="200" style="fill:#1b9df0"/><path d="M163.4 305.5c88.7 0 137.2-73.5 137.2-137.2 0-2.1 0-4.2-.1-6.2 9.4-6.8 17.6-15.3 24.1-25-8.6 3.8-17.9 6.4-27.7 7.6 10-6 17.6-15.4 21.2-26.7-9.3 5.5-19.6 9.5-30.6 11.7-8.8-9.4-21.3-15.2-35.2-15.2-26.6 0-48.2 21.6-48.2 48.2 0 3.8.4 7.5 1.3 11-40.1-2-75.6-21.2-99.4-50.4-4.1 7.1-6.5 15.4-6.5 24.2 0 16.7 8.5 31.5 21.5 40.1-7.9-.2-15.3-2.4-21.8-6v.6c0 23.4 16.6 42.8 38.7 47.3-4 1.1-8.3 1.7-12.7 1.7-3.1 0-6.1-.3-9.1-.9 6.1 19.2 23.9 33.1 45 33.5-16.5 12.9-37.3 20.6-59.9 20.6-3.9 0-7.7-.2-11.5-.7 21.1 13.8 46.5 21.8 73.7 21.8" style="fill:#fff"/></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 739 B  | 
@ -1 +1 @@
 | 
			
		||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M727.6 492.2c-34.8-6.7-17.9-25.1-17.9-25.1s34.1-55.5-6.8-95.9c-50.6-49.9-173.7 6.3-173.7 6.3-46.9 14.4-34.5-6.6-27.9-42.2 0-42-14.5-113-139.7-71.2-125.1 42.3-232.4 190.2-232.4 190.2-74.6 98.2-64.8 174.2-64.8 174.2C83 796.3 263.6 842.3 404.1 853.3c147.8 11.4 347.3-50.2 407.7-177 60.7-127-49.2-177.2-84.2-184.1zM415.1 806.1c-146.7 6.7-265.4-65.8-265.4-162.4 0-96.7 118.7-174.2 265.4-180.9 146.9-6.8 265.7 53 265.7 149.5 0 96.4-118.8 187.2-265.7 193.8z" fill="#E52429"/><path d="M799.5 433.1c12 0 22-8.7 23.8-20 .2-.9.3-1.6.3-2.5 18-159.9-132.8-132.3-132.8-132.3-13.3 0-24.1 10.6-24.1 24.1 0 13.2 10.7 23.8 24.1 23.8 108.3-23.6 84.5 83.3 84.5 83.3-.1 13 10.8 23.6 24.2 23.6z" fill="#F49500"/><path d="M782 154.1c-52.1-12.1-105.8-1.6-120.8 1.2-1.2.2-2.3 1.2-3.3 1.3-.5.2-.9.6-.9.6-14.8 4.2-25.6 17.6-25.6 33.6 0 19 15.6 34.7 35.1 34.7 0 0 18.9-2.5 31.8-7.5 12.7-5.1 120.8-3.7 174.4 85.1 29.2 64.9 12.9 108.3 10.8 115.3 0 0-7 16.9-7 33.5 0 19.1 15.6 31.3 35.1 31.3 16.2 0 29.8-2.2 33.8-29.3h.2c57.7-189.4-70.4-278.4-163.6-299.8z" fill="#F49500"/><path d="M385.9 526.3c-147.6 17-130.5 153.4-130.5 153.4s-1.5 43.2 39.5 65.2c86.3 46.2 175.3 18.2 220.1-39 45-57.1 18.7-196.5-129.1-179.6z"/><path d="M348.7 717.8c-27.6 3.1-49.8-12.6-49.8-35.3 0-22.6 19.7-46.4 47.2-49.2 31.6-3 52.2 15 52.2 37.8.2 22.7-22.2 43.6-49.6 46.7zm87-73.1c-9.4 6.9-20.9 6-25.7-2.4-5.1-8.1-3.2-20.9 6.2-27.7 10.9-8.1 22.3-5.7 27.3 2.4 4.8 8.1 1.3 20.5-7.8 27.7z" fill="#FFF"/></svg>
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 30"><g fill="none" fill-rule="evenodd"><path d="M2.715 20.11c0 4.203 5.6 7.613 12.506 7.613 6.907 0 12.506-3.41 12.506-7.613s-5.6-7.612-12.506-7.612c-6.907 0-12.506 3.409-12.506 7.612" fill="#fefefe"/><path d="M15.513 27.102c-6.114.59-11.39-2.111-11.788-6.035-.397-3.922 4.239-7.581 10.35-8.172 6.115-.591 11.39 2.11 11.789 6.032.395 3.924-4.238 7.584-10.35 8.175M27.74 14.078c-.521-.152-.878-.255-.604-.924.59-1.45.65-2.701.011-3.593-1.2-1.675-4.48-1.584-8.239-.045 0-.001-1.18.505-.878-.41.579-1.818.49-3.34-.409-4.219-2.039-1.995-7.464.075-12.115 4.62C2.023 12.914 0 16.523 0 19.643c0 5.97 7.831 9.598 15.492 9.598 10.043 0 16.724-5.702 16.724-10.231 0-2.737-2.358-4.29-4.476-4.932" fill="#d52c2b"/><path d="M34.409 3.154C31.984.526 28.405-.476 25.103.21c-.764.16-1.251.894-1.088 1.64.162.747.914 1.224 1.678 1.063 2.35-.487 4.891.226 6.617 2.093a6.687 6.687 0 0 1 1.452 6.647 1.373 1.373 0 0 0 .91 1.74c.743.235 1.54-.162 1.782-.888V12.5a9.393 9.393 0 0 0-2.045-9.346" fill="#e79115"/><path d="M30.684 6.44c-1.181-1.28-2.923-1.766-4.532-1.432a1.188 1.188 0 0 0-.935 1.413c.14.64.787 1.053 1.442.913v.002a2.366 2.366 0 0 1 2.217.698c.578.626.733 1.479.484 2.227h.002a1.187 1.187 0 0 0 .783 1.5c.64.199 1.326-.142 1.532-.768a4.57 4.57 0 0 0-.993-4.553" fill="#e79115"/><path d="M15.85 19.996c-.213.358-.686.53-1.057.38-.364-.146-.479-.545-.27-.897.212-.349.666-.52 1.03-.378.369.131.501.535.297.895m-1.947 2.445c-.593.921-1.859 1.325-2.812.9-.94-.418-1.217-1.49-.626-2.388.583-.897 1.808-1.295 2.754-.907.958.4 1.263 1.463.684 2.395m2.22-6.526c-2.909-.741-6.197.676-7.46 3.183-1.287 2.555-.042 5.392 2.897 6.32 3.043.96 6.632-.512 7.88-3.269 1.23-2.697-.306-5.474-3.317-6.234" fill="#060101"/></g></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.7 KiB  | 
| 
		 Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										1
									
								
								src/assets/icons/ui/social.youtube.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1 @@
 | 
			
		||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M711.111 512 375.467 705.422V318.578L711.11 512zM1024 694.044V329.956S1024 153.6 847.644 153.6H176.356S0 153.6 0 329.956v364.088S0 870.4 176.356 870.4h665.6c5.688 0 182.044 0 182.044-176.356" fill="#D81E06"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 325 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/masks/dot.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.1 KiB  | 
@ -41,6 +41,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div> -->
 | 
			
		||||
        <!-- // TODO: use tags instead of button, button is useless! -->
 | 
			
		||||
        <div class="row__wrapper--button" @click="handleViewPostDetailEvent">
 | 
			
		||||
          <div class="button__wrapper">
 | 
			
		||||
            <NormalButton icon="fab fa-readme" :context="buttonContext"></NormalButton>
 | 
			
		||||
@ -54,15 +55,13 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, computed } from 'vue'
 | 
			
		||||
import { useIntl, useRouter, useElementRef, useMDCRipple } from '@/hooks'
 | 
			
		||||
import postFilter from '@/utils/filters/postFilter'
 | 
			
		||||
import linkHandler from '@/utils/linkHandler'
 | 
			
		||||
import NormalButton from '@/components/buttons/NormalButton.vue'
 | 
			
		||||
// import { post as postMock } from '@/mocks/postContentMock' // mock
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  components: { NormalButton },
 | 
			
		||||
  props: {
 | 
			
		||||
    post: { type: Object /*, default: () => postMock*/ },
 | 
			
		||||
    data: { type: Object },
 | 
			
		||||
    type: { type: String, default: 'normal' }, // normal | reverse | mobile
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
@ -77,7 +76,7 @@ export default defineComponent({
 | 
			
		||||
      defaultMessage: 'Read More',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const data = computed(() => (props.post ? postFilter(props.post as Post, 'thumbList') : null))
 | 
			
		||||
    const data = props.data
 | 
			
		||||
 | 
			
		||||
    const handleViewPostDetailEvent = () => {
 | 
			
		||||
      if (data) {
 | 
			
		||||
@ -98,7 +97,7 @@ export default defineComponent({
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@use '@/styles/mixins/text';
 | 
			
		||||
@use '@/styles/mixins/tags';
 | 
			
		||||
@use '@/styles/mixins/skeleton';
 | 
			
		||||
// @use '@/styles/mixins/skeleton';
 | 
			
		||||
 | 
			
		||||
.card__container {
 | 
			
		||||
  // TODO: sizing in parent
 | 
			
		||||
@ -123,7 +122,7 @@ export default defineComponent({
 | 
			
		||||
      border-radius: 10px 0 0 10px;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      @include skeleton.skeleton-loading;
 | 
			
		||||
      // @include skeleton.skeleton-loading;
 | 
			
		||||
      .image {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										32
									
								
								src/components/cards/postThumbCards/PostThumbCardIndex.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,32 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <PostThumbCardMobile v-if="isMobile" :data="data" :type="$props.type"></PostThumbCardMobile>
 | 
			
		||||
  <PostThumbCardClassic v-else :data="data"></PostThumbCardClassic>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, computed } from 'vue'
 | 
			
		||||
import { useWindowResize } from '@/hooks'
 | 
			
		||||
import postFilter from '@/utils/filters/postFilter'
 | 
			
		||||
import PostThumbCardClassic from './PostThumbCardClassic.vue'
 | 
			
		||||
import PostThumbCardMobile from './PostThumbCardMobile.vue'
 | 
			
		||||
// import { post as postMock } from '@/mocks/postContentMock' // mock
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  components: { PostThumbCardClassic, PostThumbCardMobile },
 | 
			
		||||
  props: {
 | 
			
		||||
    post: { type: Object /*, default: () => postMock*/ },
 | 
			
		||||
    type: { type: String, default: 'normal' }, // normal | reverse | mobile
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    const windowSize = useWindowResize()
 | 
			
		||||
    const isMobile = computed(() => windowSize.value.innerWidth <= 840)
 | 
			
		||||
 | 
			
		||||
    const data = computed(() => (props.post ? postFilter(props.post as Post, 'thumbList') : null))
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      data,
 | 
			
		||||
      isMobile,
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										146
									
								
								src/components/cards/postThumbCards/PostThumbCardMobile.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,146 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="card__container">
 | 
			
		||||
    <div class="row__wrapper--thumbnail">
 | 
			
		||||
      <Image
 | 
			
		||||
        class="image"
 | 
			
		||||
        :src="data.featureImage.thumbnail"
 | 
			
		||||
        :alt="data.title"
 | 
			
		||||
        placeholder="https://via.placeholder.com/1024x768"
 | 
			
		||||
        :draggable="false"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="row__wrapper--title">
 | 
			
		||||
      <span>{{ data.title }}</span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="row__wrapper--statistics">
 | 
			
		||||
      <div class="column__wrapper--read_count">
 | 
			
		||||
        <span><i class="fab fa-hotjar"></i> {{ data.readCount }}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="column__wrapper--comment_count">
 | 
			
		||||
        <span> <i class="far fa-comment-dots"></i> {{ data.commentCount }}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="column__wrapper--word_count">
 | 
			
		||||
        <span><i class="fas fa-pen-nib"></i> {{ data.wordCount }}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="row__wrapper--abstract">
 | 
			
		||||
      <span>{{ data.excerpt }} </span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="row__wrapper--tags">
 | 
			
		||||
      <div class="tags__container">
 | 
			
		||||
        <div
 | 
			
		||||
          class="tag__wrapper"
 | 
			
		||||
          v-for="(tag, index) in ['vue', 'javascript', 'php', 'wordpress']"
 | 
			
		||||
          :key="index"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="tag yolk">
 | 
			
		||||
            <span class="text">{{ tag }}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, computed } from 'vue'
 | 
			
		||||
import { useIntl, useRouter } from '@/hooks'
 | 
			
		||||
import linkHandler from '@/utils/linkHandler'
 | 
			
		||||
import NormalButton from '@/components/buttons/NormalButton.vue'
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  components: { NormalButton },
 | 
			
		||||
  props: {
 | 
			
		||||
    data: { type: Object /*, default: () => postMock*/ },
 | 
			
		||||
    type: { type: String, default: 'normal' }, // normal | reverse | mobile
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    const intl = useIntl()
 | 
			
		||||
    const router = useRouter()
 | 
			
		||||
 | 
			
		||||
    const buttonContext = intl.formatMessage({
 | 
			
		||||
      id: 'posts.readMore',
 | 
			
		||||
      defaultMessage: 'Read More',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const data = props.data
 | 
			
		||||
 | 
			
		||||
    const handleViewPostDetailEvent = () => {
 | 
			
		||||
      if (data) {
 | 
			
		||||
        linkHandler.handleClickLink({ url: data.value?.link ?? '', router, target: '_blank' })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      data,
 | 
			
		||||
      buttonContext,
 | 
			
		||||
      handleViewPostDetailEvent,
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@use '@/styles/mixins/text';
 | 
			
		||||
@use '@/styles/mixins/tags';
 | 
			
		||||
 | 
			
		||||
.card__container {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-flow: column nowrap;
 | 
			
		||||
  justify-content: flex-start;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 12px;
 | 
			
		||||
  > * {
 | 
			
		||||
    width: calc(100% - 24px);
 | 
			
		||||
  }
 | 
			
		||||
  > .row__wrapper {
 | 
			
		||||
    &--thumbnail {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
    &--tags {
 | 
			
		||||
      max-height: 16px;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      align-items: flex-start;
 | 
			
		||||
      .tags__container {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-flow: row wrap;
 | 
			
		||||
        justify-content: flex-start;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        gap: 12px;
 | 
			
		||||
        .tag__wrapper {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          flex-flow: row nowrap;
 | 
			
		||||
          justify-content: flex-start;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          @include tags.tag-style;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &--title {
 | 
			
		||||
      line-height: 30px;
 | 
			
		||||
      font-size: x-large; // 24
 | 
			
		||||
      font-weight: bold;
 | 
			
		||||
    }
 | 
			
		||||
    &--abstract {
 | 
			
		||||
      line-height: 22px;
 | 
			
		||||
      font-size: medium; // 16
 | 
			
		||||
      @include text.line-number-limit(4);
 | 
			
		||||
    }
 | 
			
		||||
    &--statistics {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-flow: row nowrap;
 | 
			
		||||
      justify-content: space-between;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      > * {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        > span {
 | 
			
		||||
          line-height: 12px;
 | 
			
		||||
          font-size: small;
 | 
			
		||||
          color: #999999;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@ -1,7 +1,12 @@
 | 
			
		||||
<!-- # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -->
 | 
			
		||||
<!-- # Generated by scripts/import-svg-icons.js -->
 | 
			
		||||
<template>
 | 
			
		||||
  <svg-icon :data="svg[$props.name]" original></svg-icon>
 | 
			
		||||
  <svg-icon
 | 
			
		||||
    :data="svg[$props.name]"
 | 
			
		||||
    :width="$props.width"
 | 
			
		||||
    :height="$props.height"
 | 
			
		||||
    original
 | 
			
		||||
  ></svg-icon>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
import { defineComponent } from 'vue'
 | 
			
		||||
@ -24,16 +29,20 @@ import socialPixiv from '@/assets/icons/ui/social.pixiv.svg'
 | 
			
		||||
import socialQq from '@/assets/icons/ui/social.qq.svg'
 | 
			
		||||
import socialQzone from '@/assets/icons/ui/social.qzone.svg'
 | 
			
		||||
import socialStackoverflow from '@/assets/icons/ui/social.stackoverflow.svg'
 | 
			
		||||
import socialTelegram from '@/assets/icons/ui/social.telegram.svg'
 | 
			
		||||
import socialTwitch from '@/assets/icons/ui/social.twitch.svg'
 | 
			
		||||
import socialTwitter from '@/assets/icons/ui/social.twitter.svg'
 | 
			
		||||
import socialWeibo from '@/assets/icons/ui/social.weibo.svg'
 | 
			
		||||
import socialWeichat from '@/assets/icons/ui/social.weichat.svg'
 | 
			
		||||
import socialYoutube from '@/assets/icons/ui/social.youtube.svg'
 | 
			
		||||
import socialZhihu from '@/assets/icons/ui/social.zhihu.svg'
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'Icon',
 | 
			
		||||
  props: {
 | 
			
		||||
    name: { type: String },
 | 
			
		||||
    width: { type: String, default: '100%' },
 | 
			
		||||
    height: { type: String, default: '100%' },
 | 
			
		||||
  },
 | 
			
		||||
  setup() {
 | 
			
		||||
    const svg = {
 | 
			
		||||
@ -56,10 +65,12 @@ export default defineComponent({
 | 
			
		||||
      'social.qq': socialQq,
 | 
			
		||||
      'social.qzone': socialQzone,
 | 
			
		||||
      'social.stackoverflow': socialStackoverflow,
 | 
			
		||||
      'social.telegram': socialTelegram,
 | 
			
		||||
      'social.twitch': socialTwitch,
 | 
			
		||||
      'social.twitter': socialTwitter,
 | 
			
		||||
      'social.weibo': socialWeibo,
 | 
			
		||||
      'social.weichat': socialWeichat,
 | 
			
		||||
      'social.youtube': socialYoutube,
 | 
			
		||||
      'social.zhihu': socialZhihu,
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="post-thumb-list__container" :ref="setListContainerRef">
 | 
			
		||||
    <div class="post-thumb-card__wrapper" v-for="(post, index) in postList" :key="index">
 | 
			
		||||
      <span>{{ index }}: {{ post.id }}</span>
 | 
			
		||||
      <PostThumbCardClassic
 | 
			
		||||
      <!-- <span>{{ index }}: {{ post.id }}</span> -->
 | 
			
		||||
      <PostThumbCardIndex
 | 
			
		||||
        :post="post"
 | 
			
		||||
        :type="index % 2 ? 'normal' : 'reverse'"
 | 
			
		||||
      ></PostThumbCardClassic>
 | 
			
		||||
      ></PostThumbCardIndex>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="loader__wrapper" v-show="fetchStatus === 'fetching'">
 | 
			
		||||
      <BookLoader></BookLoader>
 | 
			
		||||
@ -19,11 +19,11 @@
 | 
			
		||||
import { defineComponent, computed, onMounted, Ref } from 'vue'
 | 
			
		||||
import { useInjector, useState, useElementRef, useReachElementSide } from '@/hooks'
 | 
			
		||||
import { posts } from '@/store'
 | 
			
		||||
import PostThumbCardClassic from '@/components/cards/postThumbCards/PostThumbCardClassic.vue'
 | 
			
		||||
import PostThumbCardIndex from '@/components/cards/postThumbCards/PostThumbCardIndex.vue'
 | 
			
		||||
import BookLoader from '@/components/loader/BookLoader.vue'
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  components: { PostThumbCardClassic, BookLoader },
 | 
			
		||||
  components: { PostThumbCardIndex, BookLoader },
 | 
			
		||||
  props: {
 | 
			
		||||
    namespace: { type: String, default: 'homepage' },
 | 
			
		||||
    page: { type: Number, default: 1 },
 | 
			
		||||
 | 
			
		||||
@ -76,7 +76,7 @@ export default defineComponent({
 | 
			
		||||
    const [expandContentRef, setExpandContentRef] = useElementRef()
 | 
			
		||||
    const expandContentSize = useResizeObserver(expandContentRef)
 | 
			
		||||
    const expandContentHeight = computed(() =>
 | 
			
		||||
      expandContentSize.value.height === NaN
 | 
			
		||||
      isNaN(expandContentSize.value.height)
 | 
			
		||||
        ? 0
 | 
			
		||||
        : expandContentSize.value.height + expandContentSize.value.paddingTop
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								src/hooks/lib/usePerfectScrollbar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,27 @@
 | 
			
		||||
import { ref, Ref, watch, onBeforeUnmount } from 'vue'
 | 
			
		||||
import PerfectScrollbar from 'perfect-scrollbar'
 | 
			
		||||
 | 
			
		||||
const usePerfectScrollbar = <El>(
 | 
			
		||||
  elementRef: El extends Element ? Element : Ref<Element | null>,
 | 
			
		||||
  options: PerfectScrollbar.Options = {}
 | 
			
		||||
) => {
 | 
			
		||||
  const mdcRef: Ref<PerfectScrollbar | null> = ref(null)
 | 
			
		||||
 | 
			
		||||
  if (elementRef instanceof Element) {
 | 
			
		||||
    mdcRef.value = new PerfectScrollbar(elementRef, options)
 | 
			
		||||
  } else {
 | 
			
		||||
    watch(elementRef, (element) => {
 | 
			
		||||
      if (element) {
 | 
			
		||||
        mdcRef.value = new PerfectScrollbar(element, options)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onBeforeUnmount(() => {
 | 
			
		||||
    mdcRef.value?.destroy()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return mdcRef
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default usePerfectScrollbar
 | 
			
		||||
							
								
								
									
										24
									
								
								src/hooks/mdc/useMDCList.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,24 @@
 | 
			
		||||
import { ref, Ref, watch, onBeforeUnmount } from 'vue'
 | 
			
		||||
import { MDCList } from '@material/list'
 | 
			
		||||
 | 
			
		||||
const useMDCList = <El>(elementRef: El extends Element ? Element : Ref<Element | null>) => {
 | 
			
		||||
  const mdcRef: Ref<MDCList | null> = ref(null)
 | 
			
		||||
 | 
			
		||||
  if (elementRef instanceof Element) {
 | 
			
		||||
    mdcRef.value = MDCList.attachTo(elementRef)
 | 
			
		||||
  } else {
 | 
			
		||||
    watch(elementRef, (element) => {
 | 
			
		||||
      if (element) {
 | 
			
		||||
        mdcRef.value = MDCList.attachTo(element)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onBeforeUnmount(() => {
 | 
			
		||||
    mdcRef.value?.destroy()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return mdcRef
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default useMDCList
 | 
			
		||||
@ -1,11 +1,23 @@
 | 
			
		||||
/**
 | 
			
		||||
 * https://codepen.io/gavra/pen/tEpzn
 | 
			
		||||
 */
 | 
			
		||||
import { ref, watch, Ref } from 'vue'
 | 
			
		||||
const useTypewriterEffect = (strings: string[], speed = 100): [Ref<string>, () => void] => {
 | 
			
		||||
/**
 | 
			
		||||
 * @param strings
 | 
			
		||||
 * @param speed
 | 
			
		||||
 * @param callback function to call when done
 | 
			
		||||
 * @returns [textRef, do(), done?]
 | 
			
		||||
 */
 | 
			
		||||
const useTypewriterEffect = (
 | 
			
		||||
  strings: string[],
 | 
			
		||||
  separator = '/n',
 | 
			
		||||
  speed = 100,
 | 
			
		||||
  callback?: () => void
 | 
			
		||||
): [Ref<string>, () => void, () => void, Ref<boolean>] => {
 | 
			
		||||
  const textRef = ref('')
 | 
			
		||||
  const done = ref(false)
 | 
			
		||||
 | 
			
		||||
  const typewriterEffect = () => {
 | 
			
		||||
    textRef.value = '' // reset
 | 
			
		||||
    done.value = false
 | 
			
		||||
 | 
			
		||||
    const aText = strings
 | 
			
		||||
    const iSpeed = speed // time delay of print out
 | 
			
		||||
    let iIndex = 0 // start printing array at this posision
 | 
			
		||||
@ -13,28 +25,39 @@ const useTypewriterEffect = (strings: string[], speed = 100): [Ref<string>, () =
 | 
			
		||||
    const iScrollAt = 20 // start scrolling up at this many lines
 | 
			
		||||
    let iTextPos = 0 // initialise text position
 | 
			
		||||
    let iRow // initialise current row
 | 
			
		||||
    let beforeText = '' // cache last line
 | 
			
		||||
 | 
			
		||||
    const typewriter = () => {
 | 
			
		||||
      iRow = Math.max(0, iIndex - iScrollAt)
 | 
			
		||||
      // const destination = element
 | 
			
		||||
 | 
			
		||||
      while (iRow < iIndex) {
 | 
			
		||||
        textRef.value += aText[iRow++] + '<br />'
 | 
			
		||||
        textRef.value += aText[iRow++] + separator
 | 
			
		||||
      }
 | 
			
		||||
      textRef.value = aText[iIndex].substring(0, iTextPos) // + '_'
 | 
			
		||||
      if (iTextPos++ == iArrLength) {
 | 
			
		||||
      textRef.value = beforeText + aText[iIndex].substring(0, iTextPos) // + '_'
 | 
			
		||||
      if (iTextPos++ === iArrLength) {
 | 
			
		||||
        iTextPos = 0
 | 
			
		||||
        iIndex++
 | 
			
		||||
        if (iIndex != aText.length) {
 | 
			
		||||
        if (iIndex !== aText.length) {
 | 
			
		||||
          iArrLength = aText[iIndex].length
 | 
			
		||||
          window.setTimeout(typewriter, 500)
 | 
			
		||||
          beforeText = textRef.value + separator
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        window.setTimeout(typewriter, iSpeed)
 | 
			
		||||
      }
 | 
			
		||||
      if (iRow === iIndex - 1 && iIndex === aText.length) {
 | 
			
		||||
        done.value = true
 | 
			
		||||
        if (callback) callback()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    typewriter()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [textRef, typewriterEffect]
 | 
			
		||||
  const clearTextRef = () => {
 | 
			
		||||
    textRef.value = ''
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [textRef, typewriterEffect, clearTextRef, done]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default useTypewriterEffect
 | 
			
		||||
 | 
			
		||||
@ -1,41 +1,206 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="page">
 | 
			
		||||
    <section class="header__wrapper">
 | 
			
		||||
      <Header></Header>
 | 
			
		||||
    </section>
 | 
			
		||||
    <section class="main__wrapper">
 | 
			
		||||
      <slot></slot>
 | 
			
		||||
    </section>
 | 
			
		||||
    <section class="footer__wrapper">
 | 
			
		||||
      <Footer></Footer>
 | 
			
		||||
    </section>
 | 
			
		||||
  <div>
 | 
			
		||||
    <!-- PC -->
 | 
			
		||||
    <div v-if="!isMobile" class="page">
 | 
			
		||||
      <header class="header__wrapper">
 | 
			
		||||
        <Header></Header>
 | 
			
		||||
      </header>
 | 
			
		||||
      <div class="header__placeholder" v-if="$props.headerPlaceholder"></div>
 | 
			
		||||
      <section class="content__wrapper">
 | 
			
		||||
        <slot></slot>
 | 
			
		||||
        <footer class="footer__wrapper">
 | 
			
		||||
          <Footer></Footer>
 | 
			
		||||
        </footer>
 | 
			
		||||
      </section>
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- Mobile -->
 | 
			
		||||
    <div v-else :class="['page', 'mobile', { 'show-drawer': shouldDrawerOpen }]">
 | 
			
		||||
      <header class="header__wrapper">
 | 
			
		||||
        <HeaderMobile :open="shouldDrawerOpen" @toggle="handleMDrawerToggleEvent"></HeaderMobile>
 | 
			
		||||
        <div class="fake-after" @click="handleClickFakeAfterEvent"></div>
 | 
			
		||||
      </header>
 | 
			
		||||
      <div class="header__placeholder mdc-elevation--z4" v-if="$props.headerPlaceholder"></div>
 | 
			
		||||
      <section class="content__wrapper mdc-elevation--z4">
 | 
			
		||||
        <slot></slot>
 | 
			
		||||
        <footer class="footer__wrapper">
 | 
			
		||||
          <Footer></Footer>
 | 
			
		||||
        </footer>
 | 
			
		||||
        <div class="fake-after" @click="handleClickFakeAfterEvent"></div>
 | 
			
		||||
      </section>
 | 
			
		||||
      <aside class="drawer__wrapper">
 | 
			
		||||
        <NavDrawer></NavDrawer>
 | 
			
		||||
      </aside>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue'
 | 
			
		||||
import { defineComponent, computed, onUnmounted, onDeactivated } from 'vue'
 | 
			
		||||
import { throttle, xor } from 'lodash'
 | 
			
		||||
import { useState, useWindowResize } from '@/hooks'
 | 
			
		||||
import getScrollbarWidth from '@/utils/getScrollbarWidth'
 | 
			
		||||
import Header from '@/layouts/components/header/Header.vue'
 | 
			
		||||
import Footer from '@/layouts/components/footer/Footer.vue'
 | 
			
		||||
import HeaderMobile from '@/layouts/components/header/HeaderMobile.vue'
 | 
			
		||||
import NavDrawer from '@/layouts/components/header/NavDrawer.vue'
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'LayoutBase',
 | 
			
		||||
  components: { Header, Footer },
 | 
			
		||||
  components: { Header, Footer, HeaderMobile, NavDrawer },
 | 
			
		||||
  props: { headerPlaceholder: { type: Boolean, default: true } },
 | 
			
		||||
  setup() {
 | 
			
		||||
    const windowSize = useWindowResize()
 | 
			
		||||
    const isMobile = computed(() => windowSize.value.innerWidth <= 600)
 | 
			
		||||
    const [shouldDrawerOpen, setShouldDrawerOpen] = useState(false)
 | 
			
		||||
 | 
			
		||||
    const removeScrollLock = () => {
 | 
			
		||||
      const body = document.querySelector('body')
 | 
			
		||||
      // TODO: add a fake scroll bar element
 | 
			
		||||
      if (body instanceof HTMLElement) {
 | 
			
		||||
        body.style.overflow = 'auto'
 | 
			
		||||
        body.style.width = '100%'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const addScrollLock = () => {
 | 
			
		||||
      const body = document.querySelector('body')
 | 
			
		||||
      if (body instanceof HTMLElement) {
 | 
			
		||||
        body.style.overflow = 'hidden'
 | 
			
		||||
        body.style.width = `calc(100% - ${String(getScrollbarWidth())}px)`
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const toggleDrawer = throttle(
 | 
			
		||||
      () => {
 | 
			
		||||
        setShouldDrawerOpen(!shouldDrawerOpen.value)
 | 
			
		||||
        if (shouldDrawerOpen.value) {
 | 
			
		||||
          addScrollLock()
 | 
			
		||||
        } else {
 | 
			
		||||
          removeScrollLock()
 | 
			
		||||
        }
 | 
			
		||||
        // const body = document.querySelector('body')
 | 
			
		||||
        // if (body instanceof HTMLElement) {
 | 
			
		||||
        //   body.style.overflow = xor(['hidden', 'auto'], [body.style.overflow])[0]
 | 
			
		||||
        //   body.style.width = xor(
 | 
			
		||||
        //     [`calc(100% - ${String(getScrollbarWidth())}px)`, '100%'],
 | 
			
		||||
        //     [body.style.width]
 | 
			
		||||
        //   )[0]
 | 
			
		||||
        // }
 | 
			
		||||
      },
 | 
			
		||||
      500,
 | 
			
		||||
      {
 | 
			
		||||
        trailing: false,
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const handleMDrawerToggleEvent = () => {
 | 
			
		||||
      toggleDrawer()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const handleClickFakeAfterEvent = () => {
 | 
			
		||||
      toggleDrawer()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onUnmounted(() => {
 | 
			
		||||
      setShouldDrawerOpen(false)
 | 
			
		||||
      removeScrollLock()
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    onDeactivated(() => {
 | 
			
		||||
      setShouldDrawerOpen(false)
 | 
			
		||||
      removeScrollLock()
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    return { isMobile, handleMDrawerToggleEvent, shouldDrawerOpen, handleClickFakeAfterEvent }
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
$drawer-width: 260px;
 | 
			
		||||
.page {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  .header__wrapper {
 | 
			
		||||
    position: var(--header-position, sticky);
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
    height: 48px;
 | 
			
		||||
    z-index: 2;
 | 
			
		||||
  }
 | 
			
		||||
  .main__wrapper {
 | 
			
		||||
  .header__placeholder {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 48px;
 | 
			
		||||
    visibility: hidden;
 | 
			
		||||
  }
 | 
			
		||||
  .content__wrapper {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    z-index: 0;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    background: #ffffff;
 | 
			
		||||
  }
 | 
			
		||||
  &.mobile {
 | 
			
		||||
    .header__wrapper {
 | 
			
		||||
      overflow-x: hidden; // hide box shadow
 | 
			
		||||
      height: 60px; // left the gap for box shadow
 | 
			
		||||
      ::v-deep() {
 | 
			
		||||
        .toggler__wrapper {
 | 
			
		||||
          z-index: 4;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .content__wrapper {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      z-index: 1;
 | 
			
		||||
    }
 | 
			
		||||
    .header__wrapper,
 | 
			
		||||
    .content__wrapper {
 | 
			
		||||
      transition: transform 0.5s;
 | 
			
		||||
      > .fake-after {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        right: 0;
 | 
			
		||||
        width: 0;
 | 
			
		||||
        height: 0;
 | 
			
		||||
        background: rgba(0, 0, 0, 0.2);
 | 
			
		||||
        content: '';
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
        transition: opacity 0.5s, width 0.1s 0.5s, height 0.1s 0.5s;
 | 
			
		||||
        z-index: 3;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .drawer__wrapper {
 | 
			
		||||
      position: fixed;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      width: #{$drawer-width};
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      background: #ffffff;
 | 
			
		||||
      visibility: hidden;
 | 
			
		||||
      transition: all 0.5s;
 | 
			
		||||
      z-index: 0;
 | 
			
		||||
    }
 | 
			
		||||
    &.show-drawer {
 | 
			
		||||
      .drawer__wrapper {
 | 
			
		||||
        visibility: visible;
 | 
			
		||||
        transition: transform 0.5s;
 | 
			
		||||
      }
 | 
			
		||||
      .content__wrapper,
 | 
			
		||||
      .header__wrapper {
 | 
			
		||||
        transform: translate3d(#{$drawer-width}, 0, 0);
 | 
			
		||||
        > .fake-after {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          height: 100%;
 | 
			
		||||
          opacity: 1;
 | 
			
		||||
          transition: opacity 0.5s;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      .header__wrapper {
 | 
			
		||||
        > .fake-after {
 | 
			
		||||
          height: 48px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@ -17,14 +17,19 @@
 | 
			
		||||
    <div class="content__wrapper">
 | 
			
		||||
      <div class="content__container">
 | 
			
		||||
        <div class="slogan__wrapper">
 | 
			
		||||
          <h1 class="typewriter">{{ sloganText }}</h1>
 | 
			
		||||
          <h1 class="typewriter">{{ sloganText }}<span class="cursor"> </span></h1>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="dialog__wrapper">
 | 
			
		||||
          <div class="signature__wrapper">
 | 
			
		||||
        <div :class="['dialog__wrapper', { show: shouldShowSignatureDialog }]">
 | 
			
		||||
          <div class="quote__wrapper" v-if="quote">
 | 
			
		||||
            <span>
 | 
			
		||||
              <i class="fas fa-quote-left"></i>
 | 
			
		||||
              You got to put the past behind you before you can move on.
 | 
			
		||||
              <i class="fas fa-quote-right"></i>
 | 
			
		||||
              <i class="icon fas fa-quote-left"></i>
 | 
			
		||||
              {{ quote }}
 | 
			
		||||
              <i class="icon fas fa-quote-right"></i>
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="signature__wrapper" v-if="signature">
 | 
			
		||||
            <span>
 | 
			
		||||
              {{ signature }}
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="social-media__wrapper">
 | 
			
		||||
@ -33,25 +38,44 @@
 | 
			
		||||
              v-for="(item, index) in socialMedia"
 | 
			
		||||
              :key="index"
 | 
			
		||||
            >
 | 
			
		||||
              <UiIcon :name="item.name"></UiIcon>
 | 
			
		||||
              <UiIcon :name="`social.${item.name}`"></UiIcon>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="mask__layer"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, watch } from 'vue'
 | 
			
		||||
import { defineComponent, watch, onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'
 | 
			
		||||
import { gsap } from 'gsap'
 | 
			
		||||
import { useTypewriterEffect } from '@/hooks'
 | 
			
		||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
 | 
			
		||||
import { useElementRef } from '@/hooks'
 | 
			
		||||
import { useTypewriterEffect, useState, useElementRef } from '@/hooks'
 | 
			
		||||
import sakuraOptions from '@/utils/sakuraOptions'
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  setup() {
 | 
			
		||||
    const slogan = 'Hello, world!'
 | 
			
		||||
    // sakura options data
 | 
			
		||||
    const socialMedia = Object.keys(sakuraOptions)
 | 
			
		||||
      .map((key) => {
 | 
			
		||||
        if (/^social\./.test(key)) {
 | 
			
		||||
          const match = key.match(/^social\.(.*)$/)
 | 
			
		||||
          if (!match || !sakuraOptions[key]) return null
 | 
			
		||||
          return {
 | 
			
		||||
            name: match[1],
 | 
			
		||||
            value: sakuraOptions[key],
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          return null
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .filter((i) => i)
 | 
			
		||||
 | 
			
		||||
    const slogan = sakuraOptions['homepage.slogan']
 | 
			
		||||
    const quote = sakuraOptions['homepage.quote']
 | 
			
		||||
    const signature = sakuraOptions['homepage.signature']
 | 
			
		||||
 | 
			
		||||
    const [parallaxContainerRef, setParallaxContainerRef] = useElementRef()
 | 
			
		||||
 | 
			
		||||
@ -63,6 +87,7 @@ export default defineComponent({
 | 
			
		||||
          scrollTrigger: {
 | 
			
		||||
            trigger: layersEloement[0],
 | 
			
		||||
            start: 'top top',
 | 
			
		||||
            end: 'bottom top',
 | 
			
		||||
            scrub: true,
 | 
			
		||||
          },
 | 
			
		||||
          y: '20%',
 | 
			
		||||
@ -70,32 +95,45 @@ export default defineComponent({
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const [sloganText, doSloganEffect] = useTypewriterEffect([slogan])
 | 
			
		||||
    const [shouldShowSignatureDialog, setShouldShowSignatureDialog] = useState(false)
 | 
			
		||||
    const showSignatureDialog = () => {
 | 
			
		||||
      window.setTimeout(() => setShouldShowSignatureDialog(true), 700)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [sloganText, doSloganEffect, clearText] = useTypewriterEffect(
 | 
			
		||||
      slogan.split(' '),
 | 
			
		||||
      ' ',
 | 
			
		||||
      100,
 | 
			
		||||
      showSignatureDialog
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const [isSloganEffectCalled, setIsSloganEffectCalled] = useState(false)
 | 
			
		||||
 | 
			
		||||
    const initeSloganEffect = () => {
 | 
			
		||||
      if (!isSloganEffectCalled.value) {
 | 
			
		||||
        doSloganEffect()
 | 
			
		||||
        setIsSloganEffectCalled(true)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const clearSloganEffect = () => {
 | 
			
		||||
      clearText()
 | 
			
		||||
      setShouldShowSignatureDialog(false)
 | 
			
		||||
      setIsSloganEffectCalled(false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onMounted(() => window.setTimeout(() => initeSloganEffect(), 1000))
 | 
			
		||||
    onActivated(() => window.setTimeout(() => initeSloganEffect(), 1000))
 | 
			
		||||
    onUnmounted(() => clearSloganEffect())
 | 
			
		||||
    onDeactivated(() => clearSloganEffect())
 | 
			
		||||
 | 
			
		||||
    const handleImageError = () => {
 | 
			
		||||
      window.setTimeout(() => doSloganEffect(), 1000)
 | 
			
		||||
      // window.setTimeout(() => doSloganEffect(), 1000)
 | 
			
		||||
    }
 | 
			
		||||
    const handleImageLoad = () => {
 | 
			
		||||
      window.setTimeout(() => doSloganEffect(), 1000)
 | 
			
		||||
      // window.setTimeout(() => doSloganEffect(), 1000)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const config = (window as any).InitState.config
 | 
			
		||||
    const socialMedia = Object.keys(config)
 | 
			
		||||
      .map((key) => {
 | 
			
		||||
        if (/^social\./.test(key)) {
 | 
			
		||||
          const match = key.match(/^social\.(.*)$/)
 | 
			
		||||
          if (!match || !config[key]) return null
 | 
			
		||||
          return {
 | 
			
		||||
            name: match[1],
 | 
			
		||||
            value: config[key],
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          return null
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .filter((i) => i)
 | 
			
		||||
    console.log(socialMedia)
 | 
			
		||||
 | 
			
		||||
    const styleController = () => {
 | 
			
		||||
      return {}
 | 
			
		||||
    }
 | 
			
		||||
@ -103,17 +141,20 @@ export default defineComponent({
 | 
			
		||||
    return {
 | 
			
		||||
      setParallaxContainerRef,
 | 
			
		||||
      styleController,
 | 
			
		||||
      slogan,
 | 
			
		||||
      sloganText,
 | 
			
		||||
      quote,
 | 
			
		||||
      signature,
 | 
			
		||||
      handleImageError,
 | 
			
		||||
      handleImageLoad,
 | 
			
		||||
      sloganText,
 | 
			
		||||
      socialMedia,
 | 
			
		||||
      shouldShowSignatureDialog,
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
$mobile-view-max-width: 800px;
 | 
			
		||||
.cover__container {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
@ -122,6 +163,7 @@ export default defineComponent({
 | 
			
		||||
  .background__wrapper {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    z-index: -1;
 | 
			
		||||
    .image__wrapper {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
@ -138,25 +180,40 @@ export default defineComponent({
 | 
			
		||||
      flex-flow: column nowrap;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      .slogan__wrapper {
 | 
			
		||||
        font-size: 36px;
 | 
			
		||||
        text-transform: uppercase;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
        .typewriter {
 | 
			
		||||
          overflow: hidden;
 | 
			
		||||
          border-right: 0.15em solid orange;
 | 
			
		||||
          white-space: nowrap;
 | 
			
		||||
          // position: relative;
 | 
			
		||||
          margin: 0 auto;
 | 
			
		||||
          letter-spacing: 0.15em;
 | 
			
		||||
          animation: blink-caret 0.75s step-end infinite;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes blink-caret {
 | 
			
		||||
          from,
 | 
			
		||||
          to {
 | 
			
		||||
            border-color: transparent;
 | 
			
		||||
          max-width: 80vw;
 | 
			
		||||
          align-self: center;
 | 
			
		||||
          font-size: 60px;
 | 
			
		||||
          text-transform: uppercase;
 | 
			
		||||
          color: #ffffff;
 | 
			
		||||
          white-space: nowrap;
 | 
			
		||||
          .cursor {
 | 
			
		||||
            position: relative;
 | 
			
		||||
            &:after {
 | 
			
		||||
              content: '';
 | 
			
		||||
              position: absolute;
 | 
			
		||||
              bottom: 0;
 | 
			
		||||
              left: 0;
 | 
			
		||||
              width: 0.15em;
 | 
			
		||||
              height: 1.2em;
 | 
			
		||||
              background: orange;
 | 
			
		||||
              animation: blink-caret 1s step-end infinite;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          50% {
 | 
			
		||||
            border-color: orange;
 | 
			
		||||
          @media screen and (max-width: $mobile-view-max-width) {
 | 
			
		||||
            white-space: normal;
 | 
			
		||||
          }
 | 
			
		||||
          @keyframes blink-caret {
 | 
			
		||||
            from,
 | 
			
		||||
            to {
 | 
			
		||||
              background: transparent;
 | 
			
		||||
            }
 | 
			
		||||
            50% {
 | 
			
		||||
              background: orange;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@ -170,7 +227,16 @@ export default defineComponent({
 | 
			
		||||
        // letter-spacing: 0.1em;
 | 
			
		||||
        font-size: 16px;
 | 
			
		||||
        line-height: 30px;
 | 
			
		||||
        // align-self: flex-start;
 | 
			
		||||
        align-self: flex-start;
 | 
			
		||||
        visibility: hidden;
 | 
			
		||||
        // width: 100%;
 | 
			
		||||
        @media screen and (max-width: $mobile-view-max-width) {
 | 
			
		||||
          display: none;
 | 
			
		||||
        }
 | 
			
		||||
        &.show {
 | 
			
		||||
          visibility: visible;
 | 
			
		||||
          animation: fadeIn /* animate.css */ 0.8s;
 | 
			
		||||
        }
 | 
			
		||||
        &:before {
 | 
			
		||||
          content: '';
 | 
			
		||||
          position: absolute;
 | 
			
		||||
@ -181,24 +247,60 @@ export default defineComponent({
 | 
			
		||||
          border-style: solid;
 | 
			
		||||
          border-color: transparent transparent rgba(0, 0, 0, 0.5) transparent;
 | 
			
		||||
        }
 | 
			
		||||
        .quote__wrapper {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          text-align: left;
 | 
			
		||||
          // text-align-last: right;
 | 
			
		||||
          white-space: pre;
 | 
			
		||||
          .icon {
 | 
			
		||||
            &:first-child {
 | 
			
		||||
              padding-right: 6px;
 | 
			
		||||
            }
 | 
			
		||||
            &:last-child {
 | 
			
		||||
              padding-left: 6px;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        .signature__wrapper {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          text-align: center;
 | 
			
		||||
          text-align: right;
 | 
			
		||||
        }
 | 
			
		||||
        .social-media__wrapper {
 | 
			
		||||
          margin-top: 6px;
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          display: flex;
 | 
			
		||||
          flex-flow: row wrap;
 | 
			
		||||
          justify-content: center;
 | 
			
		||||
          justify-content: flex-end;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          gap: 12px;
 | 
			
		||||
          gap: 20px;
 | 
			
		||||
          .social-media-item__wrapper {
 | 
			
		||||
            width: 20px;
 | 
			
		||||
            height: 20px;
 | 
			
		||||
            width: 24px;
 | 
			
		||||
            height: 24px;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .mask__layer {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    z-index: 0;
 | 
			
		||||
    &:before {
 | 
			
		||||
      background-image: url('@/assets/masks/dot.png');
 | 
			
		||||
      content: '';
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      bottom: 0;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      background-attachment: fixed;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
  <div class="footer__container">
 | 
			
		||||
    <div class="row__wrapper">
 | 
			
		||||
      <div class="sakura-icon__wrapper fa-spin">
 | 
			
		||||
        <ui-icon name="sakura"></ui-icon>
 | 
			
		||||
        <ui-icon name="ic.sakura"></ui-icon>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="row__wrapper">
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,14 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <header class="header__container mdc-elevation--z4">
 | 
			
		||||
  <div class="header__container mdc-elevation--z4">
 | 
			
		||||
    <div class="header__content">
 | 
			
		||||
      <div class="logo__wrapper">
 | 
			
		||||
        <img class="logo" :src="logo" alt="logo" @load="computeShouldHideNavItemList" />
 | 
			
		||||
        <img
 | 
			
		||||
          class="logo"
 | 
			
		||||
          :src="logo"
 | 
			
		||||
          alt="logo"
 | 
			
		||||
          draggable="false"
 | 
			
		||||
          @load="computeShouldHideNavItemList"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="nav__wrapper" :ref="setNavBarWrapperRef" @resize="handleNavBarWrapperResizeEvent">
 | 
			
		||||
        <div class="nav__ul nav__ul--parent" :ref="setNavBarItemRefs">
 | 
			
		||||
@ -64,11 +70,12 @@
 | 
			
		||||
        <img class="avatar" :src="avatar" alt="avatar" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </header>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, ref, watch, computed } from 'vue'
 | 
			
		||||
import { defineComponent, ref, watch, computed, onMounted } from 'vue'
 | 
			
		||||
import { debounce, cloneDeep } from 'lodash'
 | 
			
		||||
import {
 | 
			
		||||
  useElementRef,
 | 
			
		||||
  useElementRefs,
 | 
			
		||||
@ -77,17 +84,15 @@ import {
 | 
			
		||||
  useMDCRipple,
 | 
			
		||||
} from '@/hooks'
 | 
			
		||||
import { init } from '@/store'
 | 
			
		||||
import sakuraOptions from '@/utils/sakuraOptions'
 | 
			
		||||
import camelcaseKeys from 'camelcase-keys'
 | 
			
		||||
import NavItem from '@/layouts/components/header/NavItem.vue'
 | 
			
		||||
import { debounce, cloneDeep } from 'lodash'
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'Header',
 | 
			
		||||
  components: { NavItem },
 | 
			
		||||
  setup() {
 | 
			
		||||
    const avatar = 'https://view.moezx.cc/images/2021/06/13/d6b010a378d392d4633008b915f98ab1.md.png'
 | 
			
		||||
    const logo =
 | 
			
		||||
      window.InitState.config['basic.site.logo'][0]?.url || 'https://v3.vuejs.org/logo.png'
 | 
			
		||||
    const logo = sakuraOptions['basic.site.logo'][0]?.url || 'https://v3.vuejs.org/logo.png'
 | 
			
		||||
 | 
			
		||||
    const [navBarItemRefs, setNavBarItemRefs] = useElementRefs()
 | 
			
		||||
    const [navBarWrapperRef, setNavBarWrapperRef] = useElementRef()
 | 
			
		||||
@ -109,6 +114,8 @@ export default defineComponent({
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onMounted(() => computeShouldHideNavItemList())
 | 
			
		||||
 | 
			
		||||
    const navBarWrapperSize = useResizeObserver(navBarWrapperRef)
 | 
			
		||||
 | 
			
		||||
    watch(navBarWrapperSize, () => {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										140
									
								
								src/layouts/components/header/HeaderMobile.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,140 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="nav__container mdc-elevation--z4">
 | 
			
		||||
    <div class="nav__content">
 | 
			
		||||
      <div class="column__wrapper--toggler toggler__wrapper" @click="handleToggleEvent">
 | 
			
		||||
        <div :class="['toggler', { active: $props.open }]">
 | 
			
		||||
          <span></span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="column__wrapper--logo">
 | 
			
		||||
        <div class="logo__wrapper" v-if="logo">
 | 
			
		||||
          <img class="logo" :src="logo" alt="logo" draggable="false" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="sitename" v-if="sitename">{{ sitename }}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue'
 | 
			
		||||
import sakuraOptions from '@/utils/sakuraOptions'
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  props: { open: Boolean },
 | 
			
		||||
  emits: ['toggle'],
 | 
			
		||||
  setup(props, { emit }) {
 | 
			
		||||
    const handleToggleEvent = () => {
 | 
			
		||||
      emit('toggle', !props.open)
 | 
			
		||||
    }
 | 
			
		||||
    const logo = sakuraOptions['basic.site.logo'][0]?.url
 | 
			
		||||
    const sitename = sakuraOptions['basic.site.title']
 | 
			
		||||
 | 
			
		||||
    return { handleToggleEvent, logo, sitename }
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
// @use 'sass:math';
 | 
			
		||||
.nav__container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 48px;
 | 
			
		||||
  background: #ffffff;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  > .nav__content {
 | 
			
		||||
    width: calc(100% - 12px * 2);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-flow: row nowrap;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    > .column__wrapper {
 | 
			
		||||
      &--toggler {
 | 
			
		||||
        $width: 30px;
 | 
			
		||||
        $height: 3px;
 | 
			
		||||
        $color: var(--toggler-color, #333333);
 | 
			
		||||
        > .toggler {
 | 
			
		||||
          position: relative;
 | 
			
		||||
          width: #{$width};
 | 
			
		||||
          height: #{$width};
 | 
			
		||||
          cursor: pointer;
 | 
			
		||||
          user-select: none;
 | 
			
		||||
          display: flex;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          justify-content: center;
 | 
			
		||||
          > span {
 | 
			
		||||
            transition: all 0.2s ease-in-out;
 | 
			
		||||
            width: #{$width};
 | 
			
		||||
            height: #{$height};
 | 
			
		||||
            background: #{$color};
 | 
			
		||||
            position: relative;
 | 
			
		||||
            display: block;
 | 
			
		||||
            &::before {
 | 
			
		||||
              transition: all 0.2s ease-in-out;
 | 
			
		||||
              content: '';
 | 
			
		||||
              display: block;
 | 
			
		||||
              background: #{$color};
 | 
			
		||||
              height: #{$height};
 | 
			
		||||
              width: #{$width};
 | 
			
		||||
              position: absolute;
 | 
			
		||||
              // top: -16px;
 | 
			
		||||
              transform: rotate(0deg);
 | 
			
		||||
              transform-origin: 13%;
 | 
			
		||||
              top: -8px;
 | 
			
		||||
            }
 | 
			
		||||
            &::after {
 | 
			
		||||
              transition: all 0.2s ease-in-out;
 | 
			
		||||
              content: '';
 | 
			
		||||
              display: block;
 | 
			
		||||
              background: #{$color};
 | 
			
		||||
              height: #{$height};
 | 
			
		||||
              width: #{$width};
 | 
			
		||||
              position: absolute;
 | 
			
		||||
              transform: rotate(0deg);
 | 
			
		||||
              transform-origin: 13%;
 | 
			
		||||
              top: 8px;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          &.active {
 | 
			
		||||
            > span {
 | 
			
		||||
              transition: all 0.2s ease-in-out;
 | 
			
		||||
              background: transparent;
 | 
			
		||||
              &::before {
 | 
			
		||||
                transition: all 0.2s ease-in-out;
 | 
			
		||||
                transform: rotate(45deg);
 | 
			
		||||
                // width: #{$width / math.sin(45deg)};
 | 
			
		||||
              }
 | 
			
		||||
              &::after {
 | 
			
		||||
                transition: all 0.2s ease-in-out;
 | 
			
		||||
                transform: rotate(-45deg);
 | 
			
		||||
                // width: #{$width / math.sin(45deg)};
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      &--logo {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-flow: row-reverse nowrap;
 | 
			
		||||
        justify-content: flex-end;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        gap: 12px;
 | 
			
		||||
        > .logo__wrapper {
 | 
			
		||||
          flex: 0 0 auto;
 | 
			
		||||
          display: flex;
 | 
			
		||||
          justify-content: center;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          > .logo {
 | 
			
		||||
            height: 32px;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        > .sitename {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										211
									
								
								src/layouts/components/header/NavDrawer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,211 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="drawer__container" :ref="setScrollContainerRef">
 | 
			
		||||
    <div class="drawer__content">
 | 
			
		||||
      <div class="row__wrapper--avatar">
 | 
			
		||||
        <Image
 | 
			
		||||
          src="https://view.moezx.cc/images/2021/06/13/d6b010a378d392d4633008b915f98ab1.md.png"
 | 
			
		||||
          placeholder=""
 | 
			
		||||
          :avatar="true"
 | 
			
		||||
          alt=""
 | 
			
		||||
          :draggable="false"
 | 
			
		||||
        ></Image>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row__wrapper--signature">Hello world...</div>
 | 
			
		||||
      <div class="row__wrapper--social"> social</div>
 | 
			
		||||
      <div class="row__wrapper--search">
 | 
			
		||||
        <div class="background">
 | 
			
		||||
          <input class="input" type="search" name="search" placeholder="Search..." required="" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row__wrapper--menu">
 | 
			
		||||
        <div
 | 
			
		||||
          :class="['ul__wrapper', { active: currentActive === parentIndex }]"
 | 
			
		||||
          v-for="(parent, parentIndex) in navItems"
 | 
			
		||||
          :key="parentIndex"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="ul__content--tag" @click="handleClickParentEvent($event, parentIndex)">
 | 
			
		||||
            <NavItem
 | 
			
		||||
              :context="parent.title"
 | 
			
		||||
              :prefix="parent.icon"
 | 
			
		||||
              :url="parent.child.length > 0 ? '' : parent.url"
 | 
			
		||||
              :suffix="parent.child.length > 0 ? 'fas fa-chevron-down' : ''"
 | 
			
		||||
            ></NavItem>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="ul__content--child">
 | 
			
		||||
            <div class="li__wrapper" v-for="(child, childIndex) in parent.child" :key="childIndex">
 | 
			
		||||
              <div class="li__content--tag">
 | 
			
		||||
                <NavItem :context="child.title" :prefix="child.icon" :url="child.url"></NavItem>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, computed, ref } from 'vue'
 | 
			
		||||
import camelcaseKeys from 'camelcase-keys'
 | 
			
		||||
import { cloneDeep } from 'lodash'
 | 
			
		||||
import { useElementRef, useInjector } from '@/hooks'
 | 
			
		||||
import usePerfectScrollbar from '@/hooks/lib/usePerfectScrollbar'
 | 
			
		||||
import { init } from '@/store'
 | 
			
		||||
import NavItem from '@/layouts/components/header/NavItem.vue'
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  components: { NavItem },
 | 
			
		||||
  setup() {
 | 
			
		||||
    const { initState } = useInjector(init)
 | 
			
		||||
 | 
			
		||||
    const navItems = computed(() => {
 | 
			
		||||
      const items: any = []
 | 
			
		||||
      const origin = camelcaseKeys(initState.value.menus)['headerMenu'] as Array<any>
 | 
			
		||||
      origin.forEach((parent) => {
 | 
			
		||||
        if (parent.parent === 0) {
 | 
			
		||||
          const item = cloneDeep(parent)
 | 
			
		||||
          item['child'] = []
 | 
			
		||||
          origin.forEach((child) => {
 | 
			
		||||
            if (child.parent === parent.id) {
 | 
			
		||||
              item['child'].push(child)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
          items.push(item)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      return items
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const [scrollContainerRef, setScrollContainerRef] = useElementRef()
 | 
			
		||||
    const ps = usePerfectScrollbar(scrollContainerRef, { suppressScrollX: true })
 | 
			
		||||
 | 
			
		||||
    const currentActive = ref(NaN)
 | 
			
		||||
    const total = computed(() => navItems.value.length)
 | 
			
		||||
 | 
			
		||||
    const changeCurrentActive = (i: number) => {
 | 
			
		||||
      if (i === currentActive.value) currentActive.value = NaN
 | 
			
		||||
      else if (i < total.value && i >= 0) currentActive.value = i
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const handleClickParentEvent = (event: Event, i: number) => {
 | 
			
		||||
      // if has child
 | 
			
		||||
      if (navItems.value[i].child.length > 0) {
 | 
			
		||||
        changeCurrentActive(i)
 | 
			
		||||
        const collapseContainer = (event.currentTarget as HTMLElement).parentElement
 | 
			
		||||
        const collapseContent = (event.currentTarget as HTMLElement)?.nextElementSibling
 | 
			
		||||
        if (collapseContent instanceof HTMLElement) {
 | 
			
		||||
          collapseContainer?.style.setProperty(
 | 
			
		||||
            '--collapse-height',
 | 
			
		||||
            `${collapseContent.scrollHeight}px`
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log('open')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      navItems,
 | 
			
		||||
      setScrollContainerRef,
 | 
			
		||||
      currentActive,
 | 
			
		||||
      handleClickParentEvent,
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.drawer__container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  > .drawer__content {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-flow: column nowrap;
 | 
			
		||||
    justify-content: flex-start;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: 24px;
 | 
			
		||||
    > .row__wrapper {
 | 
			
		||||
      &--avatar {
 | 
			
		||||
        margin-top: 50px;
 | 
			
		||||
        width: 90px;
 | 
			
		||||
        height: 90px;
 | 
			
		||||
        border-radius: 50%;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
      }
 | 
			
		||||
      &--signature {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        color: #333333;
 | 
			
		||||
        font-weight: 900;
 | 
			
		||||
        font-family: sans-serif;
 | 
			
		||||
        letter-spacing: 1.5px;
 | 
			
		||||
      }
 | 
			
		||||
      // &--social {
 | 
			
		||||
      // }
 | 
			
		||||
      &--search {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        .background {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          border-top: 1px solid rgba(153, 153, 153, 0.3);
 | 
			
		||||
          border-bottom: 1px solid rgba(153, 153, 153, 0.3);
 | 
			
		||||
          .input {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            border: unset;
 | 
			
		||||
            padding: 8px 12px 8px 30px;
 | 
			
		||||
            outline: none;
 | 
			
		||||
            color: #666666;
 | 
			
		||||
            font-size: 16px;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      &--menu {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        > .ul__wrapper {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          > .ul__content {
 | 
			
		||||
            &--tag {
 | 
			
		||||
              width: 100%;
 | 
			
		||||
              height: 36px;
 | 
			
		||||
              background: rgba(2, 1, 1, 0);
 | 
			
		||||
              transition: all 0.3s;
 | 
			
		||||
            }
 | 
			
		||||
            &--child {
 | 
			
		||||
              max-height: 0;
 | 
			
		||||
              overflow: hidden;
 | 
			
		||||
              background: rgba(95, 93, 93, 0);
 | 
			
		||||
              transition: all 0.3s;
 | 
			
		||||
              > .li__wrapper {
 | 
			
		||||
                > .li__content--tag {
 | 
			
		||||
                  width: 100%;
 | 
			
		||||
                  height: 36px;
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          &.active {
 | 
			
		||||
            .ul__content {
 | 
			
		||||
              &--tag {
 | 
			
		||||
                background: rgba(2, 1, 1, 0.05);
 | 
			
		||||
              }
 | 
			
		||||
              &--child {
 | 
			
		||||
                max-height: var(--collapse-height);
 | 
			
		||||
                background: rgba(95, 93, 93, 0.15);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        ::v-deep() {
 | 
			
		||||
          .nav-item__content {
 | 
			
		||||
            justify-content: space-between;
 | 
			
		||||
            .context {
 | 
			
		||||
              flex: 1 1 auto;
 | 
			
		||||
              width: 100%;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { createApp } from 'vue'
 | 
			
		||||
import { VueSvgIconPlugin } from '@yzfe/vue3-svgicon'
 | 
			
		||||
import '@yzfe/svgicon/lib/svgicon.css'
 | 
			
		||||
import 'animate.css/animate.css'
 | 
			
		||||
import App from './App.vue'
 | 
			
		||||
import router from './router'
 | 
			
		||||
import { storeProviderPlugin } from './hooks/store'
 | 
			
		||||
 | 
			
		||||
@ -1 +1,4 @@
 | 
			
		||||
@use 'mdc';
 | 
			
		||||
@use './mixins/perfect-scrollbar';
 | 
			
		||||
 | 
			
		||||
@include perfect-scrollbar.core-styles;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										127
									
								
								src/styles/mixins/_perfect-scrollbar.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,127 @@
 | 
			
		||||
@mixin core-styles {
 | 
			
		||||
  /**
 | 
			
		||||
   * https://github.com/mdbootstrap/perfect-scrollbar/blob/master/css/perfect-scrollbar.css
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
 * Container style
 | 
			
		||||
 */
 | 
			
		||||
  .ps {
 | 
			
		||||
    overflow: hidden !important;
 | 
			
		||||
    overflow-anchor: none;
 | 
			
		||||
    -ms-overflow-style: none;
 | 
			
		||||
    touch-action: auto;
 | 
			
		||||
    -ms-touch-action: auto;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
 * Scrollbar rail styles
 | 
			
		||||
 */
 | 
			
		||||
  .ps__rail-x {
 | 
			
		||||
    display: none;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transition: background-color 0.2s linear, opacity 0.2s linear;
 | 
			
		||||
    -webkit-transition: background-color 0.2s linear, opacity 0.2s linear;
 | 
			
		||||
    height: 15px;
 | 
			
		||||
    /* there must be 'bottom' or 'top' for ps__rail-x */
 | 
			
		||||
    bottom: 0px;
 | 
			
		||||
    /* please don't change 'position' */
 | 
			
		||||
    position: absolute;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ps__rail-y {
 | 
			
		||||
    display: none;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transition: background-color 0.2s linear, opacity 0.2s linear;
 | 
			
		||||
    -webkit-transition: background-color 0.2s linear, opacity 0.2s linear;
 | 
			
		||||
    width: 15px;
 | 
			
		||||
    /* there must be 'right' or 'left' for ps__rail-y */
 | 
			
		||||
    right: 0;
 | 
			
		||||
    /* please don't change 'position' */
 | 
			
		||||
    position: absolute;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ps--active-x > .ps__rail-x,
 | 
			
		||||
  .ps--active-y > .ps__rail-y {
 | 
			
		||||
    display: block;
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ps:hover > .ps__rail-x,
 | 
			
		||||
  .ps:hover > .ps__rail-y,
 | 
			
		||||
  .ps--focus > .ps__rail-x,
 | 
			
		||||
  .ps--focus > .ps__rail-y,
 | 
			
		||||
  .ps--scrolling-x > .ps__rail-x,
 | 
			
		||||
  .ps--scrolling-y > .ps__rail-y {
 | 
			
		||||
    opacity: 0.6;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ps .ps__rail-x:hover,
 | 
			
		||||
  .ps .ps__rail-y:hover,
 | 
			
		||||
  .ps .ps__rail-x:focus,
 | 
			
		||||
  .ps .ps__rail-y:focus,
 | 
			
		||||
  .ps .ps__rail-x.ps--clicking,
 | 
			
		||||
  .ps .ps__rail-y.ps--clicking {
 | 
			
		||||
    background-color: #eee;
 | 
			
		||||
    opacity: 0.9;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
 * Scrollbar thumb styles
 | 
			
		||||
 */
 | 
			
		||||
  .ps__thumb-x {
 | 
			
		||||
    background-color: #aaa;
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    transition: background-color 0.2s linear, height 0.2s ease-in-out;
 | 
			
		||||
    -webkit-transition: background-color 0.2s linear, height 0.2s ease-in-out;
 | 
			
		||||
    height: 6px;
 | 
			
		||||
    /* there must be 'bottom' for ps__thumb-x */
 | 
			
		||||
    bottom: 2px;
 | 
			
		||||
    /* please don't change 'position' */
 | 
			
		||||
    position: absolute;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ps__thumb-y {
 | 
			
		||||
    background-color: #aaa;
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    transition: background-color 0.2s linear, width 0.2s ease-in-out;
 | 
			
		||||
    -webkit-transition: background-color 0.2s linear, width 0.2s ease-in-out;
 | 
			
		||||
    width: 6px;
 | 
			
		||||
    /* there must be 'right' for ps__thumb-y */
 | 
			
		||||
    right: 2px;
 | 
			
		||||
    /* please don't change 'position' */
 | 
			
		||||
    position: absolute;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ps__rail-x:hover > .ps__thumb-x,
 | 
			
		||||
  .ps__rail-x:focus > .ps__thumb-x,
 | 
			
		||||
  .ps__rail-x.ps--clicking .ps__thumb-x {
 | 
			
		||||
    background-color: #999;
 | 
			
		||||
    height: 11px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ps__rail-y:hover > .ps__thumb-y,
 | 
			
		||||
  .ps__rail-y:focus > .ps__thumb-y,
 | 
			
		||||
  .ps__rail-y.ps--clicking .ps__thumb-y {
 | 
			
		||||
    background-color: #999;
 | 
			
		||||
    width: 11px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* MS supports */
 | 
			
		||||
  @supports (-ms-overflow-style: none) {
 | 
			
		||||
    .ps {
 | 
			
		||||
      overflow: auto !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
 | 
			
		||||
    .ps {
 | 
			
		||||
      overflow: auto !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* fixup */
 | 
			
		||||
  .ps__rail-y {
 | 
			
		||||
    left: auto !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/utils/getScrollBarWidth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,15 @@
 | 
			
		||||
const getScrollbarWidth = () => {
 | 
			
		||||
  let div1 = document.createElement('div')
 | 
			
		||||
  let div2 = document.createElement('div')
 | 
			
		||||
  div1.style.width = div2.style.width = div1.style.height = div2.style.height = '100px'
 | 
			
		||||
  div1.style.overflow = 'scroll'
 | 
			
		||||
  div2.style.overflow = 'hidden'
 | 
			
		||||
  document.body.appendChild(div1)
 | 
			
		||||
  document.body.appendChild(div2)
 | 
			
		||||
  const scrollbarWidth = Math.abs(div1.scrollHeight - div2.scrollHeight)
 | 
			
		||||
  document.body.removeChild(div1)
 | 
			
		||||
  document.body.removeChild(div2)
 | 
			
		||||
  return scrollbarWidth
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default getScrollbarWidth
 | 
			
		||||
							
								
								
									
										9
									
								
								src/utils/sakuraOptions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,9 @@
 | 
			
		||||
import type { SakuraOptions as SakuraOptionsAbstract } from '@/admin/optionsType'
 | 
			
		||||
 | 
			
		||||
export interface SakuraOptions extends SakuraOptionsAbstract {
 | 
			
		||||
  [namespace: string]: any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const config = (window as any).InitState.config as SakuraOptions
 | 
			
		||||
 | 
			
		||||
export default config
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <Base class="base">
 | 
			
		||||
  <Base :headerPlaceholder="false">
 | 
			
		||||
    <div class="main__content">
 | 
			
		||||
      <div class="cover__wrapper">
 | 
			
		||||
        <Cover></Cover>
 | 
			
		||||
@ -23,14 +23,10 @@ export default defineComponent({
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.base {
 | 
			
		||||
  --header-position: fixed;
 | 
			
		||||
}
 | 
			
		||||
.main__content {
 | 
			
		||||
  .cover__wrapper {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100vh;
 | 
			
		||||
    // background: url('https://via.placeholder.com/233');
 | 
			
		||||
  }
 | 
			
		||||
  .content__wrapper {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
@ -32,10 +32,13 @@ export default defineConfig({
 | 
			
		||||
    hmr: {
 | 
			
		||||
      // TODO: .env
 | 
			
		||||
      protocol: 'ws',
 | 
			
		||||
      // host: '192.168.28.26',
 | 
			
		||||
      host: 'localhost',
 | 
			
		||||
      port: 9000,
 | 
			
		||||
    },
 | 
			
		||||
    fs: {
 | 
			
		||||
      // This maybe MDC's incorrect absolute path
 | 
			
		||||
      allow: ['./', './node_modules/', '/node_modules/'],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  build: {
 | 
			
		||||
    target: 'modules',
 | 
			
		||||
@ -55,6 +58,9 @@ export default defineConfig({
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  optimizeDeps: {
 | 
			
		||||
    include: ['gsap', 'marked', 'gsap/ScrollTrigger', 'highlight.js', '@vueuse/core'],
 | 
			
		||||
  },
 | 
			
		||||
  css: {
 | 
			
		||||
    preprocessorOptions: {
 | 
			
		||||
      scss: {
 | 
			
		||||
 | 
			
		||||