Browse Source

minimal working

main
parent
commit
13ea8c80da
11 changed files with 727 additions and 337 deletions
  1. +12
    -18
      assets/api.ts
  2. +100
    -177
      components/tabs/LookupTab.vue
  3. +31
    -68
      layouts/default.vue
  4. +55
    -3
      nuxt.config.ts
  5. +14
    -8
      package.json
  6. +3
    -5
      pages/app.vue
  7. +1
    -1
      pages/index.vue
  8. +337
    -0
      plugins/filter.ts
  9. +6
    -0
      plugins/vue-context.client.js
  10. +45
    -32
      store/index.ts
  11. +123
    -25
      yarn.lock

+ 12
- 18
assets/api.ts View File

@ -1,5 +1,7 @@
import _axios, { AxiosInstance } from 'axios'
const WANIKANI_API_KEY = 'WANIKANI_API_KEY'
export type ITypeLevel = 'character' | 'vocabulary'
export type IType = ITypeLevel | 'sentence'
@ -93,35 +95,27 @@ class Api {
}
async setWanikaniApiKey(apiKey?: string) {
apiKey = apiKey || localStorage.getItem(WANIKANI_API_KEY) || ''
console.log(apiKey)
const setApiKey = () => {
if (apiKey) {
this.axios.defaults.headers.common['Authorization'] = `Bearer ${apiKey}`
localStorage.setItem(WANIKANI_API_KEY, apiKey)
} else {
delete this.axios.defaults.headers.common['Authorization']
localStorage.removeItem(WANIKANI_API_KEY)
}
}
setApiKey()
const r = await window.$nuxt.$accessor.updateSettings()
return await this.isAuthorized().then((r) => {
if (!r) {
apiKey = ''
setApiKey()
}
return r
})
}
async isAuthorized(): Promise<boolean> {
if (this._isAuthorized && !this._user) {
const r = await this.user_getSettings({ select: ['identifier'] })
if (r) {
this._user = r.identifier
this._isAuthorized = true
}
if (!r) {
apiKey = ''
setApiKey()
}
return this._isAuthorized
return r
}
async user_getSettings(params: { select: (keyof IUserSettings)[] }) {

+ 100
- 177
components/tabs/LookupTab.vue View File

@ -30,12 +30,10 @@
<div class="column is-6 entry-display">
<div
class="character-display clickable font-han"
@click="(evt) => openContext(evt, current, 'character')"
@contextmenu.prevent="
(evt) => openContext(evt, current, 'character')
"
@click="(evt) => openContext(evt, entry)"
@contextmenu.prevent="(evt) => openContext(evt, entry)"
>
{{ current }}
{{ entry.entry }}
</div>
<div class="buttons has-addons">
@ -57,7 +55,7 @@
</button>
</div>
<div v-if="tag.length" class="mb-4">
<div v-if="entry.tag.length" class="mb-4">
Tags:
<b-taglist style="display: inline-flex">
<b-tag v-for="t in tag.slice(0, 5)" :key="t" type="is-info">
@ -66,11 +64,11 @@
</b-taglist>
</div>
<div v-if="level && level <= 60" class="mb-4">
<div v-if="entry.level <= 60" class="mb-4">
<b-taglist attached>
<b-tag type="is-dark">Level</b-tag>
<b-tag type="is-primary">
{{ level }}
{{ entry.level }}
</b-tag>
</b-taglist>
</div>
@ -91,14 +89,14 @@
</div>
<div class="card-content">
{{ reading.join(' / ') }}
{{ entry.reading.join(' / ') }}
</div>
</b-collapse>
<b-collapse
class="card"
animation="slide"
:open="!!translation.length"
:open="!!entry.translation.length"
>
<div
slot="trigger"
@ -113,11 +111,11 @@
</div>
<div class="card-content">
{{ translation.join(' / ') }}
{{ entry.translation.join(' / ') }}
</div>
</b-collapse>
<b-collapse class="card" animation="slide" :open="!!sub.length">
<b-collapse class="card" animation="slide" :open="!!entry.sub.length">
<div
slot="trigger"
slot-scope="props"
@ -132,18 +130,22 @@
<div class="card-content">
<span
v-for="h in sub"
v-for="h in entry.sub"
:key="h"
class="font-han clickable"
@click="(evt) => openContext(evt, h, 'character')"
@contextmenu.prevent="(evt) => openContext(evt, h, 'character')"
@click="
(evt) => openContext(evt, { entry: h, type: 'character' })
"
@contextmenu.prevent="
(evt) => openContext(evt, { entry: h, type: 'character' })
"
>
{{ h }}
</span>
</div>
</b-collapse>
<b-collapse class="card" animation="slide" :open="!!sup.length">
<b-collapse class="card" animation="slide" :open="!!entry.sup.length">
<div
slot="trigger"
slot-scope="props"
@ -158,18 +160,26 @@
<div class="card-content">
<span
v-for="h in sup"
v-for="h in entry.sup"
:key="h"
class="font-han clickable"
@click="(evt) => openContext(evt, h, 'character')"
@contextmenu.prevent="(evt) => openContext(evt, h, 'character')"
@click="
(evt) => openContext(evt, { entry: h, type: 'character' })
"
@contextmenu.prevent="
(evt) => openContext(evt, { entry: h, type: 'character' })
"
>
{{ h }}
</span>
</div>
</b-collapse>
<b-collapse class="card" animation="slide" :open="!!variants.length">
<b-collapse
class="card"
animation="slide"
:open="!!entry.variant.length"
>
<div
slot="trigger"
slot-scope="props"
@ -184,82 +194,20 @@
<div class="card-content">
<span
v-for="h in variants"
v-for="h in entry.variant"
:key="h"
class="font-han clickable"
@click="(evt) => openContext(evt, h, 'character')"
@contextmenu.prevent="(evt) => openContext(evt, h, 'character')"
@click="
(evt) => openContext(evt, { entry: h, type: 'character' })
"
@contextmenu.prevent="
(evt) => openContext(evt, { entry: h, type: 'character' })
"
>
{{ h }}
</span>
</div>
</b-collapse>
<b-collapse class="card" animation="slide" :open="!!vocabs.length">
<div
slot="trigger"
slot-scope="props"
class="card-header"
role="button"
>
<h2 class="card-header-title">Vocabularies</h2>
<a role="button" class="card-header-icon">
<fontawesome :icon="props.open ? 'caret-down' : 'caret-up'" />
</a>
</div>
<div class="card-content">
<div v-for="(v, i) in vocabs" :key="i" class="long-item">
<span
class="clickable"
@click="(evt) => openContext(evt, v.entry, 'vocabulary')"
@contextmenu.prevent="
(evt) => openContext(evt, v.entry, 'vocabulary')
"
>
{{ v.entry }}
</span>
<span v-if="v.alt" class="clickable">
{{ v.alt.join(' ') }}
</span>
<span class="pinyin">[{{ v.reading.join(' / ') }}]</span>
<span>{{ v.translation.join(' / ') }}</span>
</div>
</div>
</b-collapse>
<b-collapse class="card" animation="slide" :open="!!sentences.length">
<div
slot="trigger"
slot-scope="props"
class="card-header"
role="button"
>
<h2 class="card-header-title">Sentences</h2>
<a role="button" class="card-header-icon">
<fontawesome :icon="props.open ? 'caret-down' : 'caret-up'" />
</a>
</div>
<div class="card-content">
<div v-for="(s, i) in sentences" :key="i" class="long-item">
<span
class="clickable"
@click="(evt) => openContext(evt, s.entry[0], 'sentence')"
@contextmenu.prevent="
(evt) => openContext(evt, s.entry[0], 'sentence')
"
>
{{ s.entry[0] }}
</span>
<span>{{ s.translation[0] }}</span>
</div>
</div>
</b-collapse>
</div>
</div>
</div>
@ -278,10 +226,31 @@
import { Component, Prop, Ref, Vue, Watch } from 'nuxt-property-decorator'
import ContextMenu from '@/components/ContextMenu.vue'
import { api, IDictionaryLookup, IEntryLookup, IType } from '~/assets/api'
import { capitalize } from '~/assets/util'
type ILookupEntry = Omit<IDictionaryLookup, 'id'> & IEntryLookup
const makeLookupEntry = (
entry = '',
type: IType = 'vocabulary'
): ILookupEntry => {
return {
entry,
list: [],
type,
reading: [],
translation: [],
audio: [],
description: '',
tag: [],
sub: [],
sup: [],
variant: [],
visual: [],
amalgamation: [],
component: [],
}
}
@Component<LookupTab>({
components: {
ContextMenu,
@ -300,13 +269,12 @@ export default class LookupTab extends Vue {
@Prop({ default: () => ({}) }) query!: {
q?: string
entry?: string
type?: 'character' | 'vocabulary'
}
@Prop() type!: 'character' | 'vocabulary'
@Ref() context!: ContextMenu
title = this.type === 'character' ? 'Kanji' : capitalize(this.type)
title = 'Lookup'
entries: ILookupEntry[] = []
i = 0
@ -320,6 +288,7 @@ export default class LookupTab extends Vue {
selected: {
entry: string
type: string
audio?: ILookupEntry['audio']
} = {
entry: '',
type: '',
@ -328,8 +297,8 @@ export default class LookupTab extends Vue {
q = ''
q0 = ''
get current() {
return this.entries[this.i]?.entry || ''
get entry() {
return this.entries[this.i] || makeLookupEntry()
}
get additionalContext() {
@ -338,7 +307,7 @@ export default class LookupTab extends Vue {
{
name: 'Reload',
handler: async () => {
const { result } = await api.all_getRandom({ type: this.type })
const { result } = await api.all_getRandom({ type: 'vocabulary' })
this.q0 = result
},
},
@ -348,12 +317,8 @@ export default class LookupTab extends Vue {
return []
}
openContext(
evt: MouseEvent,
entry = this.selected.entry,
type = this.selected.type
) {
this.selected = { entry, type }
openContext(evt: MouseEvent, entry = this.selected) {
this.selected = entry
this.context.open(evt)
}
@ -362,72 +327,23 @@ export default class LookupTab extends Vue {
this.$emit('title', (q ? q + ' - ' : '') + this.title)
this.entries = []
if (this.type === 'character') {
if (/\p{sc=Han}/u.test(q)) {
let qs = q.split('').filter((h) => /\p{sc=Han}/u.test(h))
qs = qs.filter((h, i) => qs.indexOf(h) === i)
this.entries = qs.map(
(entry) =>
({
entry,
list: [],
type: this.type,
reading: [],
translation: [],
audio: [],
description: '',
tag: [],
sub: [],
sup: [],
variant: [],
visual: [],
amalgamation: [],
component: [],
} as ILookupEntry)
)
} else {
if (!q.trim()) {
const { result } = await api.all_getRandom({ type: this.type })
this.q0 = result
this.q = result
return
} else {
const r = await api.all_getQuery({
q,
type: this.type,
distinct: 'id',
})
this.entries = r.result.map(
({ entry }) =>
({
entry,
list: [],
type: this.type,
reading: [],
translation: [],
audio: [],
description: '',
tag: [],
sub: [],
sup: [],
variant: [],
visual: [],
amalgamation: [],
component: [],
} as ILookupEntry)
)
}
}
if (q.trim()) {
const r = await api.all_getQuery({
q,
type: this.query.type,
distinct: 'id',
})
this.entries = r.result.map(({ entry, type }) =>
makeLookupEntry(entry, type)
)
}
this.i = 0
}
@Watch('current')
@Watch('entry.entry')
load() {
if (this.current) {
if (this.entry.entry) {
this.loadComponents()
this.loadVocabularies()
this.loadSentences()
@ -441,17 +357,18 @@ export default class LookupTab extends Vue {
}
async loadComponents() {
if (!this.current.trim()) return
const entry = this.entries[this.i]
if (!entry) return
const [comp, dict] = await Promise.all([
api.all_getComponent({
entry: this.current,
type: this.type,
entry: entry.entry,
type: entry.type,
}),
api
.all_getLookup({
entry: this.current,
type: this.type,
entry: entry.entry,
type: entry.type,
limit: 1,
})
.then(({ result: [r] }) => r),
@ -459,9 +376,9 @@ export default class LookupTab extends Vue {
this.$set(this.entries, this.i, {
...(dict || {
entry: this.current,
entry: entry.entry,
list: [],
type: this.type,
type: entry.type,
reading: [],
translation: [],
audio: [],
@ -480,22 +397,25 @@ export default class LookupTab extends Vue {
}
async loadVocabularies() {
if (this.type === 'character') {
if (!/^\p{sc=Han}$/u.test(this.current)) {
const entry = this.entries[this.i]
if (!entry) return
if (entry.type === 'character') {
if (!/^\p{sc=Han}$/u.test(entry.entry)) {
this.lookup.vocabulary = []
return
}
this.lookup.vocabulary = await api
.character_getVocabulary({
entry: this.current,
entry: entry.entry,
distinct: 'id',
})
.then((r) => r.result)
} else {
this.lookup.vocabulary = await api
.vocabulary_getVocabulary({
entry: this.current,
entry: entry.entry,
distinct: 'id',
})
.then((r) => r.result)
@ -503,22 +423,25 @@ export default class LookupTab extends Vue {
}
async loadSentences() {
if (this.type === 'character') {
if (!/^\p{sc=Han}$/u.test(this.current)) {
const entry = this.entries[this.i]
if (!entry) return
if (entry.type === 'character') {
if (!/^\p{sc=Han}$/u.test(entry.entry)) {
this.lookup.sentence = []
return
}
this.lookup.sentence = await api
.character_getSentence({
entry: this.current,
entry: entry.entry,
distinct: 'id',
})
.then((r) => r.result)
} else {
this.lookup.sentence = await api
.vocabulary_getSentence({
entry: this.current,
entry: entry.entry,
distinct: 'id',
})
.then((r) => r.result)

+ 31
- 68
layouts/default.vue View File

@ -1,74 +1,37 @@
<template>
<div>
<nav
class="navbar header has-shadow is-primary"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand">
<a
class="navbar-item"
href="/"
>
<img
src="~assets/buefy.png"
alt="Buefy"
height="28"
>
</a>
<div class="navbar-burger">
<span />
<span />
<span />
</div>
</div>
</nav>
<section class="main-content columns">
<aside class="column is-2 section">
<p class="menu-label is-hidden-touch">
General
</p>
<ul class="menu-list">
<li
v-for="(item, key) of items"
:key="key"
>
<NuxtLink
:to="item.to"
exact-active-class="is-active"
>
<b-icon :icon="item.icon" /> {{ item.title }}
</NuxtLink>
</li>
</ul>
</aside>
<div class="container column is-10">
<Nuxt />
</div>
</section>
<div class="DefaultLayout">
<nuxt />
</div>
</template>
<script>
export default {
data () {
return {
items: [
{
title: 'Home',
icon: 'home',
to: { name: 'index' }
},
{
title: 'Inspire',
icon: 'lightbulb',
to: { name: 'inspire' }
}
]
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import '~/assets/speak'
@Component<DefaultLayout>({
mounted() {
if (process.env['NODE_ENV'] !== 'development') {
window.onbeforeunload = function (e: any) {
e.preventDefault()
e.returnValue = 'not returned'
}
}
}
}
},
})
export default class DefaultLayout extends Vue {}
</script>
<style lang="scss">
html,
body,
#__nuxt,
#__layout,
.DefaultLayout {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
overscroll-behavior-y: none;
}
</style>

+ 55
- 3
nuxt.config.ts View File

@ -1,6 +1,8 @@
import { NuxtConfig } from '@nuxt/types'
export default async (): Promise<NuxtConfig> => {
const NODE_ENV = process.env['NODE_ENV'] || ''
return {
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
ssr: false,
@ -10,11 +12,15 @@ export default async (): Promise => {
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'jaquiz',
title: 'WaniKani-based jaquiz',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{
hid: 'description',
name: 'description',
content: 'WaniKani-based jaquiz',
},
{ name: 'format-detection', content: 'telephone=no' },
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
@ -24,7 +30,7 @@ export default async (): Promise => {
css: [],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [],
plugins: ['~/plugins/filter.ts', '~/plugins/vue-context.client.js'],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
@ -33,6 +39,7 @@ export default async (): Promise => {
buildModules: [
// https://go.nuxtjs.dev/typescript
'@nuxt/typescript-build',
'nuxt-typed-vuex',
],
// Modules: https://go.nuxtjs.dev/config-modules
@ -41,6 +48,43 @@ export default async (): Promise => {
'nuxt-buefy',
// https://go.nuxtjs.dev/pwa
'@nuxtjs/pwa',
'@nuxtjs/proxy',
[
'@nuxtjs/fontawesome',
{
component: 'fontawesome',
icons: {
brands: ['faGithub'],
solid: [
'faAngleLeft',
'faAngleRight',
'faAngleUp',
'faArrowUp',
'faBookReader',
'faCaretDown',
'faCaretUp',
'faChalkboardTeacher',
'faCog',
'faExclamationCircle',
'faExclamationTriangle',
'faEye',
'faEyeSlash',
'faInfoCircle',
'faRandom',
'faSearch',
'faTag',
'faListUl',
'faListOl',
'faBars',
'faSave',
'faTrash',
'faTools',
'faTh',
'faRedoAlt',
],
},
},
],
],
// PWA module configuration: https://go.nuxtjs.dev/pwa
@ -50,6 +94,14 @@ export default async (): Promise => {
},
},
proxy: {
'/api': 'http://localhost:35594',
},
env: {
NODE_ENV,
},
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {},
}

+ 14
- 8
package.json View File

@ -9,22 +9,28 @@
"generate": "nuxt generate"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^2.0.6",
"@nuxtjs/fontawesome": "^1.1.2",
"@nuxtjs/proxy": "^2.1.0",
"@nuxtjs/pwa": "^3.3.5",
"axios": "^0.23.0",
"core-js": "^3.15.1",
"axios": "^0.24.0",
"core-js": "^3.19.0",
"jsonschema-definer": "^1.3.2",
"nuxt": "^2.15.7",
"nuxt-buefy": "^0.4.8",
"nuxt": "^2.15.8",
"nuxt-buefy": "^0.4.11",
"nuxt-property-decorator": "^2.9.1",
"nuxt-typed-vuex": "^0.2.0",
"vue-context": "^6.0.0"
},
"devDependencies": {
"@nuxt/types": "^2.15.7",
"@nuxt/types": "^2.15.8",
"@nuxt/typescript-build": "^2.1.0",
"eslint-config-prettier": "^8.3.0",
"prettier": "^2.3.2",
"sass": "^1.43.3",
"prettier": "^2.4.1",
"sass": "^1.43.4",
"sass-loader": "~10.1.1"
}
}
}

+ 3
- 5
pages/app.vue View File

@ -129,16 +129,14 @@ import { api } from '~/assets/api'
LibraryTab,
},
async mounted() {
await this.$accessor.updateSettings()
if (!this.$accessor.isApp) {
this.$router.replace('/')
} else {
if (await api.setWanikaniApiKey()) {
this.$accessor.ADD_TAB({
component: 'Lookup',
first: true,
})
this.isReady = true
} else {
this.$router.replace('/')
}
},
})

+ 1
- 1
pages/index.vue View File

@ -23,7 +23,7 @@ import { api } from '~/assets/api'
@Component<HomePage>({
async mounted() {
if (await api.isAuthorized()) {
if (await api.setWanikaniApiKey()) {
this.$router.replace('/app')
} else {
this.isReady = true

+ 337
- 0
plugins/filter.ts View File

@ -0,0 +1,337 @@
import Vue from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Vue.filter('format', (v: any) => {
if (typeof v === 'number') {
return v || v === 0 ? v.toLocaleString() : ''
} else if (v instanceof Date) {
return v.toLocaleDateString([], {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'short',
})
} else if (v && typeof v === 'object') {
return JSON.stringify(v)
}
return v
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Vue.filter('formatDate', (v: any) => {
return v
? new Date(v).toLocaleDateString([], {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'short',
})
: ''
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Vue.filter('duration', (v: any) => {
return v
? new Duration(new Date(v), new Date()).toString({
sign: false,
granularity: 2,
})
: ''
})
/**
* Possible duration units in this parser
*/
export type DurationUnit = 'ms' | 's' | 'min' | 'h' | 'd' | 'w' | 'mo' | 'y'
export interface IDurationOptions {
/**
* @default true
*/
sign?: boolean
/**
* Number of units plus subunits
*/
granularity?: number
/**
* Number of max units shown
*/
maxUnit?: number
/**
* Smallest unit shown
*
* @default 's'
*/
smallest?: DurationUnit
/**
* Custom naming for units
*/
unit?: Partial<Record<DurationUnit, string>>
}
export class Duration {
/**
* Sign in front of the output toString()
*/
sign: '+' | '-' | '' = '+'
/**
* Milliseconds
*/
ms: number
/**
* Seconds
*/
s: number
/**
* Minutes
*/
min: number
/**
* Hours
*/
h: number
/**
* Days
*/
d: number
/**
* Weeks
*/
w: number
/**
* Months
*/
mo: number
/**
* Years
*/
y: number
private _dates: [Date, Date] = [new Date(this.from), new Date(this.to)]
/**
* Parse milliseconds (i.e. epoch) to Duration, based on before present time
*/
static of(msec: number) {
const to = new Date()
const output = new this(new Date(+to - msec), to)
output.sign = ''
return output
}
constructor(
/**
* Starting Date
*/
public from: Date,
/**
* Ending date
*/
public to: Date
) {
if (from > to) {
this.sign = '-'
this._dates = this._dates.reverse() as [Date, Date]
}
this.ms = this._parse((d) => d.getMilliseconds(), {
get: (d) => d.getSeconds(),
set: (d, v) => d.setSeconds(v),
inc: () => 1000,
})
this.s = this._parse((d) => d.getSeconds(), {
get: (d) => d.getMinutes(),
set: (d, v) => d.setMinutes(v),
inc: () => 60,
})
this.min = this._parse((d) => d.getMinutes(), {
get: (d) => d.getHours(),
set: (d, v) => d.setHours(v),
inc: () => 60,
})
this.h = this._parse((d) => d.getHours(), {
get: (d) => d.getDate(),
set: (d, v) => d.setDate(v),
inc: () => 24,
})
this.d = this._parse((d) => d.getDate(), {
get: (d) => d.getMonth(),
set: (d, v) => d.setMonth(v),
inc: (d) => {
const y = d.getFullYear()
let isLeapYear = true
if (y % 4) {
isLeapYear = false
} else if (y % 100) {
isLeapYear = true
} else if (y % 400) {
isLeapYear = false
}
return [
31, // Jan
isLeapYear ? 29 : 28, // Feb
31, // Mar
30, // Apr
31, // May
30, // Jun
31, // Jul
31, // Aug
30, // Sep
31, // Oct
30, // Nov
31, // Dec
][d.getMonth()]!
},
})
this.w = Math.floor(this.d / 7)
this.d = this.d % 7
this.mo = this._parse((d) => d.getMonth(), {
get: (d) => d.getFullYear(),
set: (d, v) => d.setFullYear(v),
inc: () => 12,
})
this.y = this._parse((d) => d.getFullYear())
}
/**
* To JSON-serializable OrderedDict
*/
toOrderedDict(): [DurationUnit, number][] {
return [
['ms', this.ms],
['s', this.s],
['min', this.min],
['h', this.h],
['d', this.d],
['w', this.w],
['mo', this.mo],
['y', this.y],
]
}
/**
* To String
*
* Works with `${duration}` also
*/
toString({
sign = true,
granularity,
maxUnit,
smallest = 's',
unit = {},
}: IDurationOptions = {}) {
const odict = this.toOrderedDict()
const smallestIndex = odict.map(([k]) => k).indexOf(smallest)
const filteredDict = odict.filter(([, v], i) => v && i >= smallestIndex)
const str = filteredDict
.slice(granularity ? filteredDict.length - granularity : 0)
.reverse()
.slice(0, maxUnit)
.map(([k, v]) => `${v.toLocaleString()}${unit[k] || k}`)
.join(' ')
if (sign) {
return this.sign + str
}
return str
}
private _parse(
current: (d: Date) => number,
upper?: {
get: (d: Date) => number
set: (d: Date, v: number) => void
inc: (d: Date) => number
}
) {
let a = current(this._dates[1]) - current(this._dates[0])
if (upper) {
while (a < 0) {
upper.set(this._dates[1], upper.get(this._dates[1]) - 1)
this._dates[1] = new Date(this._dates[1])
a += upper.inc(this._dates[1])
}
}
return a
}
}
/**
* Date adding functions
*/
export function addDate(d: Date): Record<DurationUnit, (n: number) => Date> {
return {
/**
* Milliseconds
*/
ms: (n) => {
d.setMilliseconds(d.getMilliseconds() + n)
return new Date(d)
},
/**
* Seconds
*/
s: (n) => {
d.setSeconds(d.getSeconds() + n)
return new Date(d)
},
/**
* Minutes
*/
min: (n) => {
d.setMinutes(d.getMinutes() + n)
return new Date(d)
},
/**
* Hours
*/
h: (n) => {
d.setHours(d.getHours() + n)
return new Date(d)
},
/**
* Date
*/
d: (n) => {
d.setDate(d.getDate() + n)
return new Date(d)
},
/**
* Weeks
*/
w: (n) => {
d.setDate(d.getDate() + n * 7)
return new Date(d)
},
/**
* Months
*/
mo: (n) => {
d.setMonth(d.getMonth() + n)
return new Date(d)
},
/**
* Years
*/
y: (n) => {
d.setFullYear(d.getFullYear() + n)
return new Date(d)
},
}
}

+ 6
- 0
plugins/vue-context.client.js View File

@ -0,0 +1,6 @@
import 'vue-context/dist/css/vue-context.css'
import Vue from 'vue'
import VueContext from 'vue-context'
Vue.component('VueContext', VueContext)

+ 45
- 32
store/index.ts View File

@ -1,4 +1,5 @@
import { actionTree, getAccessorType, mutationTree } from 'typed-vuex'
import { api } from '~/assets/api'
import { jsonClone } from '~/assets/util'
@ -29,6 +30,24 @@ export const state = () => ({
identifier: '',
})
const SET_ALL_TABS = (
s: ReturnType<typeof state>,
o: {
tabs: ITab[]
activeTab: number
} = s
) => {
localStorage.setItem(
TAB_SETTINGS,
JSON.stringify({
tabs: o.tabs,
activeTabs: o.activeTab,
})
)
s.tabs = jsonClone(o.tabs)
s.activeTab = jsonClone(o.activeTab)
}
export const mutations = mutationTree(state, {
SET_SETTINGS(state, settings: ISettings) {
state.settings = settings
@ -36,26 +55,17 @@ export const mutations = mutationTree(state, {
SET_IS_APP(state, isApp: boolean) {
state.isApp = isApp
},
SET_ALL_TABS(
state,
o: {
tabs: ITab[]
activeTab: number
} = state
) {
localStorage.setItem(
TAB_SETTINGS,
JSON.stringify({
tabs: o.tabs,
activeTabs: o.activeTab,
})
)
state.tabs = jsonClone(o.tabs)
state.activeTab = jsonClone(o.activeTab)
},
SET_TAB_TITLE(state, { i, title }: { i: number; title: string }) {
state.tabs[i].title = title
this.SET_ALL_TABS(state)
if (state.tabs[i]) {
state.tabs[i]!.title = title
SET_ALL_TABS(state)
}
},
SET_ALL_TABS(state) {
const settings = localStorage.getItem(TAB_SETTINGS)
if (settings) {
Object.assign(state, JSON.parse(settings))
}
},
ADD_TAB(
state,
@ -90,7 +100,7 @@ export const mutations = mutationTree(state, {
}
state.activeTab = state.tabs.length - 1
this.SET_ALL_TABS(state)
SET_ALL_TABS(state)
},
DELETE_TAB(state, { i }: { i: number }) {
state.tabs.splice(i, 1)
@ -99,15 +109,17 @@ export const mutations = mutationTree(state, {
state.activeTab = state.tabs.length - 1
}
this.SET_ALL_TABS(state)
SET_ALL_TABS(state)
},
SET_TAB_COMPONENT(state, { i, component }: { i: number; component: string }) {
state.tabs[i].component = component
this.SET_ALL_TABS(state)
if (state.tabs[i]) {
state.tabs[i]!.component = component
SET_ALL_TABS(state)
}
},
SET_TAB_CURRENT(state, { i }: { i: number }) {
state.activeTab = i
this.SET_ALL_TABS(state)
SET_ALL_TABS(state)
},
SET_IDENTIFIER(state, name: string) {
state.identifier = name
@ -117,16 +129,15 @@ export const mutations = mutationTree(state, {
export const actions = actionTree(
{ state, mutations },
{
async nuxtServerInit({ dispatch, commit }) {
await dispatch('updateSetings')
const settings = localStorage.getItem(TAB_SETTINGS)
if (settings) commit('SET_ALL_TABS', JSON.parse(settings))
async nuxtServerInit({ commit }) {
commit('SET_ALL_TABS')
},
async updateSettings({ commit }) {
const r = await api.user_getSettings({
select: ['identifier', 'levelMin', 'level'],
})
const r = await api
.user_getSettings({
select: ['identifier', 'levelMin', 'level'],
})
.catch(() => null)
commit('SET_IS_APP', !!r)
commit('SET_IDENTIFIER', r?.identifier || '')
@ -144,6 +155,8 @@ export const actions = actionTree(
level: r?.level || null,
levelMin: r?.levelMin || null,
})
return !!r
},
}
)

+ 123
- 25
yarn.lock View File

@ -908,6 +908,42 @@
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
"@fortawesome/fontawesome-common-types@^0.2.36":
version "0.2.36"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz#b44e52db3b6b20523e0c57ef8c42d315532cb903"
integrity sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==
"@fortawesome/fontawesome-svg-core@^1.2.27", "@fortawesome/fontawesome-svg-core@^1.2.36":
version "1.2.36"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz#4f2ea6f778298e0c47c6524ce2e7fd58eb6930e3"
integrity sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.36"
"@fortawesome/free-brands-svg-icons@^5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.4.tgz#ec8a44dd383bcdd58aa7d1c96f38251e6fec9733"
integrity sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.36"
"@fortawesome/free-solid-svg-icons@^5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz#2a68f3fc3ddda12e52645654142b9e4e8fbb6cc5"
integrity sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.36"
"@fortawesome/vue-fontawesome@^0.1.9":
version "0.1.10"
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-0.1.10.tgz#eeeec1e4e8850bed0468f938292b06cda793bf34"
integrity sha512-b2+SLF31h32LSepVcXe+BQ63yvbq5qmTCy4KfFogCYm2bn68H5sDWUnX+U7MBqnM2aeEk9M7xSoqGnu+wSdY6w==
"@fortawesome/vue-fontawesome@^2.0.6":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.6.tgz#87e691ed87f28f4667238573a29743f543a087f6"
integrity sha512-V3vT3flY15AKbUS31aZOP12awQI3aAzkr2B1KnqcHLmwrmy51DW3pwyBczKdypV8QxBZ8U68Hl2XxK2nudTxpg==
"@gar/promisify@^1.0.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210"
@ -1165,7 +1201,7 @@
rc9 "^1.2.0"
std-env "^2.3.0"
"@nuxt/types@^2.15.7":
"@nuxt/types@^2.15.8":
version "2.15.8"
resolved "https://registry.yarnpkg.com/@nuxt/types/-/types-2.15.8.tgz#1249de448f68169fe17e9379ee7b5caa0eb336b0"
integrity sha512-zBAG5Fy+SIaZIerOVF1vxy1zz16ZK07QSbsuQAjdtEFlvr+vKK+0AqCv8r8DBY5IVqdMIaw5FgNUz5py0xWdPg==
@ -1302,6 +1338,21 @@
webpack-node-externals "^3.0.0"
webpackbar "^4.0.0"
"@nuxtjs/fontawesome@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@nuxtjs/fontawesome/-/fontawesome-1.1.2.tgz#0add6519095b392bdffb6e3ad40f3026d20f5c44"
integrity sha512-QAfo7hdc6hiCOohdR861oNQ+riKW/kD22bYyvaC++xXiiC1hBQcrRQ6xXd5gln+6SKCwT09+C4kGjzTgrwtr7w==
dependencies:
"@fortawesome/fontawesome-svg-core" "^1.2.27"
"@fortawesome/vue-fontawesome" "^0.1.9"
"@nuxtjs/proxy@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@nuxtjs/proxy/-/proxy-2.1.0.tgz#fa7715a11d237fa1273503c4e9e137dd1bf5575b"
integrity sha512-/qtoeqXgZ4Mg6LRg/gDUZQrFpOlOdHrol/vQYMnKu3aN3bP90UfOUB3QSDghUUK7OISAJ0xp8Ld78aHyCTcKCQ==
dependencies:
http-proxy-middleware "^1.0.6"
"@nuxtjs/pwa@^3.3.5":
version "3.3.5"
resolved "https://registry.yarnpkg.com/@nuxtjs/pwa/-/pwa-3.3.5.tgz#db7c905536ebe8a464a347b6ae3215810642c044"
@ -1470,6 +1521,13 @@
"@types/relateurl" "*"
"@types/uglify-js" "*"
"@types/http-proxy@^1.17.5":
version "1.17.7"
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.7.tgz#30ea85cc2c868368352a37f0d0d3581e24834c6f"
integrity sha512-9hdj6iXH64tHSLTY+Vt2eYOGzSogC+JQ2H7bdPWkuh7KXP5qLllWx++t+K9Wk556c3dkDdPws/SpMRi0sdCT1w==
dependencies:
"@types/node" "*"
"@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8":
version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
@ -2121,10 +2179,10 @@ autoprefixer@^9.6.1:
postcss "^7.0.32"
postcss-value-parser "^4.1.0"
axios@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.23.0.tgz#b0fa5d0948a8d1d75e3d5635238b6c4625b05149"
integrity sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==
axios@^0.24.0:
version "0.24.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
dependencies:
follow-redirects "^1.14.4"
@ -2356,10 +2414,10 @@ browserslist@*, browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.16.6,
node-releases "^2.0.1"
picocolors "^1.0.0"
buefy@^0.9.10:
version "0.9.10"
resolved "https://registry.yarnpkg.com/buefy/-/buefy-0.9.10.tgz#17f64ee1ba43a145d1d3c56f45cba95e4e2975fa"
integrity sha512-xXEoy/NTgBNiIfBTCdHi2Vu5SJJdB046py6ekUvYuUgYwRvulySZksdecVNNWdfEVU8iD4esZaRbTLwCegFcVQ==
buefy@^0.9.11:
version "0.9.11"
resolved "https://registry.yarnpkg.com/buefy/-/buefy-0.9.11.tgz#69ab7f46e4f172d43247916f770f29764f9e0931"
integrity sha512-WP32SiaM9WVxDtzgdiq7V2zyIvn41NboPgluVqdB6OAi1/QhjO/63m6hd/jy6Vk8r+zuhIZD+aP9KlQ10EhxTQ==
dependencies:
bulma "0.9.3"
@ -2894,10 +2952,10 @@ core-js@^2.6.5:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.15.1:
version "3.18.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.3.tgz#86a0bba2d8ec3df860fefcc07a8d119779f01509"
integrity sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw==
core-js@^3.19.0:
version "3.19.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.19.0.tgz#9e40098a9bc326c7e81b486abbd5e12b9d275176"
integrity sha512-L1TpFRWXZ76vH1yLM+z6KssLZrP8Z6GxxW4auoCj+XiViOzNPJCAuTIkn03BGdFe6Z5clX5t64wRIRypsZQrUg==
core-util-is@~1.0.0:
version "1.0.3"
@ -3599,6 +3657,11 @@ etag@^1.8.1, etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
eventemitter3@^4.0.0:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
events@^3.0.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
@ -3837,6 +3900,11 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
follow-redirects@^1.0.0:
version "1.14.5"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381"
integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==
follow-redirects@^1.14.4:
version "1.14.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
@ -4302,6 +4370,26 @@ http-errors@~1.7.2:
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-proxy-middleware@^1.0.6:
version "1.3.1"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz#43700d6d9eecb7419bf086a128d0f7205d9eb665"
integrity sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==
dependencies:
"@types/http-proxy" "^1.17.5"
http-proxy "^1.18.1"
is-glob "^4.0.1"
is-plain-obj "^3.0.0"
micromatch "^4.0.2"
http-proxy@^1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
dependencies:
eventemitter3 "^4.0.0"
follow-redirects "^1.0.0"
requires-port "^1.0.0"
https-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
@ -4658,6 +4746,11 @@ is-plain-obj@^1.0.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
is-plain-obj@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@ -5158,7 +5251,7 @@ micromatch@^3.1.10, micromatch@^3.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.2"
micromatch@^4.0.0, micromatch@^4.0.4:
micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
@ -5517,12 +5610,12 @@ num2fraction@^1.2.2:
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
nuxt-buefy@^0.4.8:
version "0.4.10"
resolved "https://registry.yarnpkg.com/nuxt-buefy/-/nuxt-buefy-0.4.10.tgz#7e091ebee5c7dd2bf9de46a3b7a36a3a4d0d4fba"
integrity sha512-Kk/QrNXUrIe3/iPecb0v1OmcZWfdihioEuPtqW4OUfaDDPrVUdfPPRKLMHNgy1OPxDEGSor/h3zMr23d9/0Nyw==
nuxt-buefy@^0.4.11:
version "0.4.11"
resolved "https://registry.yarnpkg.com/nuxt-buefy/-/nuxt-buefy-0.4.11.tgz#38a4d2a3abae05c64034d005e46728cfd694fa25"
integrity sha512-mld92vqY5uo+aQp4E8OnVE0abXPgf5t+k4ckmsMgYGfn0T3pDqJq7O4JLELxfBxfmBxmwce8lJ6ZaJ2GxCNaWQ==
dependencies:
buefy "^0.9.10"
buefy "^0.9.11"
nuxt-property-decorator@^2.9.1:
version "2.9.1"
@ -5542,7 +5635,7 @@ nuxt-typed-vuex@^0.2.0:
typed-vuex "0.2.0"
upath "^2.0.1"
nuxt@^2.15.7:
nuxt@^2.15.8:
version "2.15.8"
resolved "https://registry.yarnpkg.com/nuxt/-/nuxt-2.15.8.tgz#946cba46bdaaf0e3918aa27fd9ea0fed8ed303b0"
integrity sha512-ceK3qLg/Baj7J8mK9bIxqw9AavrF+LXqwYEreBdY/a4Sj8YV4mIvhqea/6E7VTCNNGvKT2sJ/TTJjtfQ597lTA==
@ -6600,7 +6693,7 @@ prettier@^1.18.2:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
prettier@^2.3.2:
prettier@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
@ -6925,6 +7018,11 @@ repeat-string@^1.6.1:
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resolve-from@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
@ -7062,10 +7160,10 @@ sass-loader@10.1.1, sass-loader@~10.1.1:
schema-utils "^3.0.0"
semver "^7.3.2"
sass@^1.43.3:
version "1.43.3"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.3.tgz#aa16a69131b84f0cd23189a242571e8905f1ce43"
integrity sha512-BJnLngqWpMeS65UvlYYEuCb3/fLxDxhHtOB/gWPxs6NKrslTxGt3ZxwIvOe/0Jm4tWwM/+tIpE3wj4dLEhPDeQ==
sass@^1.43.4:
version "1.43.4"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.4.tgz#68c7d6a1b004bef49af0d9caf750e9b252105d1f"
integrity sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==
dependencies:
chokidar ">=3.0.0 <4.0.0"