mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-08 23:25:03 -04:00
Merge remote-tracking branch 'CorentinTh/main' into main
This commit is contained in:
commit
4447ffee3c
207 changed files with 7280 additions and 3237 deletions
|
@ -22,7 +22,9 @@
|
||||||
"createGlobalState": true,
|
"createGlobalState": true,
|
||||||
"createInjectionState": true,
|
"createInjectionState": true,
|
||||||
"createReactiveFn": true,
|
"createReactiveFn": true,
|
||||||
|
"createReusableTemplate": true,
|
||||||
"createSharedComposable": true,
|
"createSharedComposable": true,
|
||||||
|
"createTemplatePromise": true,
|
||||||
"createUnrefFn": true,
|
"createUnrefFn": true,
|
||||||
"customRef": true,
|
"customRef": true,
|
||||||
"debouncedRef": true,
|
"debouncedRef": true,
|
||||||
|
@ -42,9 +44,6 @@
|
||||||
"isReactive": true,
|
"isReactive": true,
|
||||||
"isReadonly": true,
|
"isReadonly": true,
|
||||||
"isRef": true,
|
"isRef": true,
|
||||||
"logicAnd": true,
|
|
||||||
"logicNot": true,
|
|
||||||
"logicOr": true,
|
|
||||||
"makeDestructurable": true,
|
"makeDestructurable": true,
|
||||||
"markRaw": true,
|
"markRaw": true,
|
||||||
"nextTick": true,
|
"nextTick": true,
|
||||||
|
@ -97,6 +96,7 @@
|
||||||
"toReactive": true,
|
"toReactive": true,
|
||||||
"toRef": true,
|
"toRef": true,
|
||||||
"toRefs": true,
|
"toRefs": true,
|
||||||
|
"toValue": true,
|
||||||
"triggerRef": true,
|
"triggerRef": true,
|
||||||
"tryOnBeforeMount": true,
|
"tryOnBeforeMount": true,
|
||||||
"tryOnBeforeUnmount": true,
|
"tryOnBeforeUnmount": true,
|
||||||
|
@ -107,6 +107,19 @@
|
||||||
"unrefElement": true,
|
"unrefElement": true,
|
||||||
"until": true,
|
"until": true,
|
||||||
"useActiveElement": true,
|
"useActiveElement": true,
|
||||||
|
"useAnimate": true,
|
||||||
|
"useArrayDifference": true,
|
||||||
|
"useArrayEvery": true,
|
||||||
|
"useArrayFilter": true,
|
||||||
|
"useArrayFind": true,
|
||||||
|
"useArrayFindIndex": true,
|
||||||
|
"useArrayFindLast": true,
|
||||||
|
"useArrayIncludes": true,
|
||||||
|
"useArrayJoin": true,
|
||||||
|
"useArrayMap": true,
|
||||||
|
"useArrayReduce": true,
|
||||||
|
"useArraySome": true,
|
||||||
|
"useArrayUnique": true,
|
||||||
"useAsyncQueue": true,
|
"useAsyncQueue": true,
|
||||||
"useAsyncState": true,
|
"useAsyncState": true,
|
||||||
"useAttrs": true,
|
"useAttrs": true,
|
||||||
|
@ -117,8 +130,8 @@
|
||||||
"useBroadcastChannel": true,
|
"useBroadcastChannel": true,
|
||||||
"useBrowserLocation": true,
|
"useBrowserLocation": true,
|
||||||
"useCached": true,
|
"useCached": true,
|
||||||
"useClamp": true,
|
|
||||||
"useClipboard": true,
|
"useClipboard": true,
|
||||||
|
"useCloned": true,
|
||||||
"useColorMode": true,
|
"useColorMode": true,
|
||||||
"useConfirmDialog": true,
|
"useConfirmDialog": true,
|
||||||
"useCounter": true,
|
"useCounter": true,
|
||||||
|
@ -192,12 +205,18 @@
|
||||||
"useOnline": true,
|
"useOnline": true,
|
||||||
"usePageLeave": true,
|
"usePageLeave": true,
|
||||||
"useParallax": true,
|
"useParallax": true,
|
||||||
|
"useParentElement": true,
|
||||||
|
"usePerformanceObserver": true,
|
||||||
"usePermission": true,
|
"usePermission": true,
|
||||||
"usePointer": true,
|
"usePointer": true,
|
||||||
|
"usePointerLock": true,
|
||||||
"usePointerSwipe": true,
|
"usePointerSwipe": true,
|
||||||
"usePreferredColorScheme": true,
|
"usePreferredColorScheme": true,
|
||||||
|
"usePreferredContrast": true,
|
||||||
"usePreferredDark": true,
|
"usePreferredDark": true,
|
||||||
"usePreferredLanguages": true,
|
"usePreferredLanguages": true,
|
||||||
|
"usePreferredReducedMotion": true,
|
||||||
|
"usePrevious": true,
|
||||||
"useRafFn": true,
|
"useRafFn": true,
|
||||||
"useRefHistory": true,
|
"useRefHistory": true,
|
||||||
"useResizeObserver": true,
|
"useResizeObserver": true,
|
||||||
|
@ -211,14 +230,17 @@
|
||||||
"useSessionStorage": true,
|
"useSessionStorage": true,
|
||||||
"useShare": true,
|
"useShare": true,
|
||||||
"useSlots": true,
|
"useSlots": true,
|
||||||
|
"useSorted": true,
|
||||||
"useSpeechRecognition": true,
|
"useSpeechRecognition": true,
|
||||||
"useSpeechSynthesis": true,
|
"useSpeechSynthesis": true,
|
||||||
"useStepper": true,
|
"useStepper": true,
|
||||||
"useStorage": true,
|
"useStorage": true,
|
||||||
"useStorageAsync": true,
|
"useStorageAsync": true,
|
||||||
"useStyleTag": true,
|
"useStyleTag": true,
|
||||||
|
"useSupported": true,
|
||||||
"useSwipe": true,
|
"useSwipe": true,
|
||||||
"useTemplateRefsList": true,
|
"useTemplateRefsList": true,
|
||||||
|
"useTextDirection": true,
|
||||||
"useTextSelection": true,
|
"useTextSelection": true,
|
||||||
"useTextareaAutosize": true,
|
"useTextareaAutosize": true,
|
||||||
"useThrottle": true,
|
"useThrottle": true,
|
||||||
|
@ -230,6 +252,8 @@
|
||||||
"useTimeoutPoll": true,
|
"useTimeoutPoll": true,
|
||||||
"useTimestamp": true,
|
"useTimestamp": true,
|
||||||
"useTitle": true,
|
"useTitle": true,
|
||||||
|
"useToNumber": true,
|
||||||
|
"useToString": true,
|
||||||
"useToggle": true,
|
"useToggle": true,
|
||||||
"useTransition": true,
|
"useTransition": true,
|
||||||
"useUrlSearchParams": true,
|
"useUrlSearchParams": true,
|
||||||
|
@ -250,8 +274,10 @@
|
||||||
"watchArray": true,
|
"watchArray": true,
|
||||||
"watchAtMost": true,
|
"watchAtMost": true,
|
||||||
"watchDebounced": true,
|
"watchDebounced": true,
|
||||||
|
"watchDeep": true,
|
||||||
"watchEffect": true,
|
"watchEffect": true,
|
||||||
"watchIgnorable": true,
|
"watchIgnorable": true,
|
||||||
|
"watchImmediate": true,
|
||||||
"watchOnce": true,
|
"watchOnce": true,
|
||||||
"watchPausable": true,
|
"watchPausable": true,
|
||||||
"watchPostEffect": true,
|
"watchPostEffect": true,
|
||||||
|
|
|
@ -1,39 +1,14 @@
|
||||||
/* eslint-env node */
|
/**
|
||||||
require('@rushstack/eslint-patch/modern-module-resolution');
|
* @type {import('eslint').Linter.Config}
|
||||||
|
*/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: [
|
extends: ['@antfu', './.eslintrc-auto-import.json', '@unocss'],
|
||||||
'plugin:vue/vue3-essential',
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:vue/vue3-recommended',
|
|
||||||
'@vue/eslint-config-typescript/recommended',
|
|
||||||
'@vue/eslint-config-prettier',
|
|
||||||
'plugin:import/recommended',
|
|
||||||
'./.eslintrc-auto-import.json',
|
|
||||||
'@unocss',
|
|
||||||
],
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
'import/resolver': { typescript: { project: './tsconfig.app.json' } },
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
'vue/setup-compiler-macros': true,
|
|
||||||
},
|
|
||||||
rules: {
|
rules: {
|
||||||
'vue/multi-word-component-names': ['off'],
|
'curly': ['error', 'all'],
|
||||||
'prettier/prettier': ['error'],
|
'@typescript-eslint/semi': ['error', 'always'],
|
||||||
'import/no-duplicates': ['error', { considerQueryString: true }],
|
'@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
|
||||||
'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }],
|
'vue/no-empty-component-block': ['error'],
|
||||||
'import/extensions': [
|
|
||||||
'error',
|
|
||||||
'ignorePackages',
|
|
||||||
{
|
|
||||||
js: 'never',
|
|
||||||
ts: 'never',
|
|
||||||
tsx: 'never',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'import/no-unresolved': ['error', { ignore: ['^virtual:'] }],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
4
.github/ISSUE_TEMPLATE/new-tool-request.md
vendored
4
.github/ISSUE_TEMPLATE/new-tool-request.md
vendored
|
@ -6,8 +6,8 @@ labels: new tool
|
||||||
assignees: CorentinTh
|
assignees: CorentinTh
|
||||||
---
|
---
|
||||||
|
|
||||||
**Which tool is impacted?**
|
**What tool do you want?**
|
||||||
Example: the token generator
|
Example: a token generator
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Describe the solution you'd like**
|
||||||
A clear and concise description of what you want to happen.
|
A clear and concise description of what you want to happen.
|
||||||
|
|
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
@ -27,5 +27,8 @@ jobs:
|
||||||
- name: Run unit test
|
- name: Run unit test
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: pnpm typecheck
|
||||||
|
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
39
CHANGELOG.md
39
CHANGELOG.md
|
@ -2,6 +2,45 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## Version 2023.05.14-77f2efc
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **list-converter**: a small converter who deals with column based data and do some stuff with it (#387) (83a7b3b)
|
||||||
|
- **new tool**: phone parser and normalizer (ce3150c)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- **phone-parser**: use default country code (a43c546)
|
||||||
|
- **home**: prevent weird blue border on card (3f6c8f0)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
- **ui**: replaced some n-input with c-input-text (77f2efc)
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
- **issues**: updated new tool request issue template (edae4c6)
|
||||||
|
|
||||||
|
### Ui-lib
|
||||||
|
- **new-component**: added text input component in the c-lib (aad8d84)
|
||||||
|
- **button**: size variants (401f13f)
|
||||||
|
|
||||||
|
## Version 2023.04.23-92bd835
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **ui-lib**: demo pages for c-lib components (92bd835)
|
||||||
|
- **new-tool**: diff of two json objects (362f2fa)
|
||||||
|
- **ipv4-range-expander**: expands a given IPv4 start and end address to a valid IPv4 subnet (#366) (df989e2)
|
||||||
|
- **date converter**: auto focus main input (6d22025)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- **ts**: cleaned legacy typechecking warning (e88c1d5)
|
||||||
|
- **mac-address-lookup**: added copy handler on button click (c311e38)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
- **ui-lib**: prevent c-button to shrink (61ece23)
|
||||||
|
- **ui**: replaced naive ui cards with custom ones (f080933)
|
||||||
|
- **clean**: removed unused lodash import (bb32513)
|
||||||
|
- **clean**: removed useless br tags (74073f5)
|
||||||
|
- **ui**: getting ride of naive ui buttons (c45bce3)
|
||||||
|
|
||||||
## Version 2023.04.14-dbad773
|
## Version 2023.04.14-dbad773
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
@ -84,7 +84,7 @@ To create a new tool, there is a script that generate the boilerplate of the new
|
||||||
pnpm run script:create-new-tool my-tool-name
|
pnpm run script:create-new-tool my-tool-name
|
||||||
```
|
```
|
||||||
|
|
||||||
It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the inported tool in the proper category and develop the tool.
|
It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the imported tool in the proper category and develop the tool.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
|
|
72
auto-imports.d.ts
vendored
72
auto-imports.d.ts
vendored
|
@ -19,7 +19,9 @@ declare global {
|
||||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||||
|
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||||
|
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||||
const customRef: typeof import('vue')['customRef']
|
const customRef: typeof import('vue')['customRef']
|
||||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||||
|
@ -39,9 +41,6 @@ declare global {
|
||||||
const isReactive: typeof import('vue')['isReactive']
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
const isReadonly: typeof import('vue')['isReadonly']
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
const isRef: typeof import('vue')['isRef']
|
const isRef: typeof import('vue')['isRef']
|
||||||
const logicAnd: typeof import('@vueuse/core')['logicAnd']
|
|
||||||
const logicNot: typeof import('@vueuse/core')['logicNot']
|
|
||||||
const logicOr: typeof import('@vueuse/core')['logicOr']
|
|
||||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
@ -92,8 +91,9 @@ declare global {
|
||||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||||
const toRaw: typeof import('vue')['toRaw']
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||||
const toRef: typeof import('vue')['toRef']
|
const toRef: typeof import('@vueuse/core')['toRef']
|
||||||
const toRefs: typeof import('vue')['toRefs']
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('@vueuse/core')['toValue']
|
||||||
const triggerRef: typeof import('vue')['triggerRef']
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||||
|
@ -104,6 +104,19 @@ declare global {
|
||||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||||
const until: typeof import('@vueuse/core')['until']
|
const until: typeof import('@vueuse/core')['until']
|
||||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||||
|
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||||
|
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||||
|
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||||
|
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||||
|
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
||||||
|
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
||||||
|
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
|
||||||
|
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
|
||||||
|
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
||||||
|
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
||||||
|
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
||||||
|
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
||||||
|
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
|
||||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||||
const useAttrs: typeof import('vue')['useAttrs']
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
@ -114,8 +127,8 @@ declare global {
|
||||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||||
const useCached: typeof import('@vueuse/core')['useCached']
|
const useCached: typeof import('@vueuse/core')['useCached']
|
||||||
const useClamp: typeof import('@vueuse/core')['useClamp']
|
|
||||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||||
|
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||||
|
@ -189,12 +202,18 @@ declare global {
|
||||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||||
|
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
||||||
|
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
||||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||||
|
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
||||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||||
|
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
||||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||||
|
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||||
|
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||||
|
@ -208,14 +227,17 @@ declare global {
|
||||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||||
const useShare: typeof import('@vueuse/core')['useShare']
|
const useShare: typeof import('@vueuse/core')['useShare']
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||||
|
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||||
|
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||||
|
@ -227,6 +249,8 @@ declare global {
|
||||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||||
|
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
||||||
|
const useToString: typeof import('@vueuse/core')['useToString']
|
||||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||||
|
@ -247,8 +271,10 @@ declare global {
|
||||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||||
|
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
||||||
const watchEffect: typeof import('vue')['watchEffect']
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||||
|
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
||||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
|
@ -282,7 +308,9 @@ declare module 'vue' {
|
||||||
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
||||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||||
|
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||||
|
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||||
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||||
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
||||||
|
@ -302,9 +330,6 @@ declare module 'vue' {
|
||||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||||
readonly logicAnd: UnwrapRef<typeof import('@vueuse/core')['logicAnd']>
|
|
||||||
readonly logicNot: UnwrapRef<typeof import('@vueuse/core')['logicNot']>
|
|
||||||
readonly logicOr: UnwrapRef<typeof import('@vueuse/core')['logicOr']>
|
|
||||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||||
|
@ -355,8 +380,9 @@ declare module 'vue' {
|
||||||
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
|
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
|
||||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||||
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
||||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
readonly toRef: UnwrapRef<typeof import('@vueuse/core')['toRef']>
|
||||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||||
|
readonly toValue: UnwrapRef<typeof import('@vueuse/core')['toValue']>
|
||||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||||
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||||
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
||||||
|
@ -367,6 +393,19 @@ declare module 'vue' {
|
||||||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||||
|
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||||
|
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||||
|
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||||
|
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||||
|
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
|
||||||
|
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
|
||||||
|
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
|
||||||
|
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
|
||||||
|
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
|
||||||
|
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
|
||||||
|
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
|
||||||
|
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
|
||||||
|
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
|
||||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||||
|
@ -377,8 +416,8 @@ declare module 'vue' {
|
||||||
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
|
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
|
||||||
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
|
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
|
||||||
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
|
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
|
||||||
readonly useClamp: UnwrapRef<typeof import('@vueuse/core')['useClamp']>
|
|
||||||
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
||||||
|
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||||
|
@ -452,12 +491,18 @@ declare module 'vue' {
|
||||||
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||||
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||||
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||||
|
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
||||||
|
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
|
||||||
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
||||||
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
||||||
|
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
|
||||||
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
||||||
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
||||||
|
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
|
||||||
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||||
|
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||||
|
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||||
|
@ -471,14 +516,17 @@ declare module 'vue' {
|
||||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||||
|
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||||
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
||||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||||
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||||
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
||||||
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||||
|
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||||
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||||
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
||||||
|
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
|
||||||
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
||||||
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
|
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
|
||||||
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
|
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
|
||||||
|
@ -490,6 +538,8 @@ declare module 'vue' {
|
||||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||||
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
|
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
|
||||||
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
|
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
|
||||||
|
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
|
||||||
|
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
|
||||||
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
|
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
|
||||||
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
||||||
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
||||||
|
@ -510,8 +560,10 @@ declare module 'vue' {
|
||||||
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
||||||
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
||||||
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
||||||
|
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
|
||||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||||
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
||||||
|
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
|
||||||
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
||||||
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
||||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||||
|
|
102
components.d.ts
vendored
102
components.d.ts
vendored
|
@ -9,18 +9,90 @@ export {}
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
'404.page': typeof import('./src/pages/404.page.vue')['default']
|
||||||
|
About: typeof import('./src/pages/About.vue')['default']
|
||||||
|
App: typeof import('./src/App.vue')['default']
|
||||||
|
'Base.layout': typeof import('./src/layouts/base.layout.vue')['default']
|
||||||
|
Base64FileConverter: typeof import('./src/tools/base64-file-converter/base64-file-converter.vue')['default']
|
||||||
|
Base64StringConverter: typeof import('./src/tools/base64-string-converter/base64-string-converter.vue')['default']
|
||||||
|
BasicAuthGenerator: typeof import('./src/tools/basic-auth-generator/basic-auth-generator.vue')['default']
|
||||||
|
Bcrypt: typeof import('./src/tools/bcrypt/bcrypt.vue')['default']
|
||||||
|
BenchmarkBuilder: typeof import('./src/tools/benchmark-builder/benchmark-builder.vue')['default']
|
||||||
|
Bip39Generator: typeof import('./src/tools/bip39-generator/bip39-generator.vue')['default']
|
||||||
|
CAlert: typeof import('./src/ui/c-alert/c-alert.vue')['default']
|
||||||
|
'CAlert.demo': typeof import('./src/ui/c-alert/c-alert.demo.vue')['default']
|
||||||
|
CameraRecorder: typeof import('./src/tools/camera-recorder/camera-recorder.vue')['default']
|
||||||
|
CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default']
|
||||||
|
CButton: typeof import('./src/ui/c-button/c-button.vue')['default']
|
||||||
|
'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default']
|
||||||
|
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
|
||||||
|
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
|
||||||
|
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
|
||||||
|
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
|
||||||
|
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
|
||||||
|
'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default']
|
||||||
|
CLink: typeof import('./src/ui/c-link/c-link.vue')['default']
|
||||||
|
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
|
||||||
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
|
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
|
||||||
|
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
|
||||||
ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
|
ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
|
||||||
|
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
|
||||||
|
DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default']
|
||||||
|
DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default']
|
||||||
|
DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default']
|
||||||
|
DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default']
|
||||||
|
DockerRunToDockerComposeConverter: typeof import('./src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue')['default']
|
||||||
|
DynamicValues: typeof import('./src/tools/benchmark-builder/dynamic-values.vue')['default']
|
||||||
|
Editor: typeof import('./src/tools/html-wysiwyg-editor/editor/editor.vue')['default']
|
||||||
|
Encryption: typeof import('./src/tools/encryption/encryption.vue')['default']
|
||||||
|
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
|
||||||
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
|
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
|
||||||
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
|
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
|
||||||
|
GitMemo: typeof import('./src/tools/git-memo/git-memo.md')['default']
|
||||||
|
HashText: typeof import('./src/tools/hash-text/hash-text.vue')['default']
|
||||||
|
HmacGenerator: typeof import('./src/tools/hmac-generator/hmac-generator.vue')['default']
|
||||||
|
'Home.page': typeof import('./src/pages/Home.page.vue')['default']
|
||||||
|
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
|
||||||
|
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
|
||||||
|
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
|
||||||
|
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
|
||||||
|
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
|
||||||
|
IconMdiClose: typeof import('~icons/mdi/close')['default']
|
||||||
|
IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
|
||||||
|
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
|
||||||
|
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
||||||
|
IconMdiEye: typeof import('~icons/mdi/eye')['default']
|
||||||
|
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
|
||||||
|
IconMdiPause: typeof import('~icons/mdi/pause')['default']
|
||||||
|
IconMdiPlay: typeof import('~icons/mdi/play')['default']
|
||||||
|
IconMdiRecord: typeof import('~icons/mdi/record')['default']
|
||||||
|
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||||
|
IconMdiVideo: typeof import('~icons/mdi/video')['default']
|
||||||
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
||||||
|
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
|
||||||
|
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
|
||||||
|
Ipv4RangeExpander: typeof import('./src/tools/ipv4-range-expander/ipv4-range-expander.vue')['default']
|
||||||
|
Ipv4SubnetCalculator: typeof import('./src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue')['default']
|
||||||
|
Ipv6UlaGenerator: typeof import('./src/tools/ipv6-ula-generator/ipv6-ula-generator.vue')['default']
|
||||||
|
JsonDiff: typeof import('./src/tools/json-diff/json-diff.vue')['default']
|
||||||
|
JsonMinify: typeof import('./src/tools/json-minify/json-minify.vue')['default']
|
||||||
|
JsonToYaml: typeof import('./src/tools/json-to-yaml-converter/json-to-yaml.vue')['default']
|
||||||
|
JsonViewer: typeof import('./src/tools/json-viewer/json-viewer.vue')['default']
|
||||||
|
JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
|
||||||
|
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default']
|
||||||
|
ListConverter: typeof import('./src/tools/list-converter/list-converter.vue')['default']
|
||||||
|
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
|
||||||
|
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
|
||||||
|
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
|
||||||
|
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
|
||||||
|
MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default']
|
||||||
MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default']
|
MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default']
|
||||||
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
|
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
|
||||||
|
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
|
||||||
|
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
|
||||||
NAlert: typeof import('naive-ui')['NAlert']
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
NAutoComplete: typeof import('naive-ui')['NAutoComplete']
|
NAutoComplete: typeof import('naive-ui')['NAutoComplete']
|
||||||
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
|
||||||
NCard: typeof import('naive-ui')['NCard']
|
|
||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
NCode: typeof import('naive-ui')['NCode']
|
NCode: typeof import('naive-ui')['NCode']
|
||||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||||
|
@ -37,7 +109,6 @@ declare module '@vue/runtime-core' {
|
||||||
NH1: typeof import('naive-ui')['NH1']
|
NH1: typeof import('naive-ui')['NH1']
|
||||||
NH2: typeof import('naive-ui')['NH2']
|
NH2: typeof import('naive-ui')['NH2']
|
||||||
NH3: typeof import('naive-ui')['NH3']
|
NH3: typeof import('naive-ui')['NH3']
|
||||||
NH4: typeof import('naive-ui')['NH4']
|
|
||||||
NIcon: typeof import('naive-ui')['NIcon']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
NImage: typeof import('naive-ui')['NImage']
|
NImage: typeof import('naive-ui')['NImage']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
|
@ -50,11 +121,9 @@ declare module '@vue/runtime-core' {
|
||||||
NP: typeof import('naive-ui')['NP']
|
NP: typeof import('naive-ui')['NP']
|
||||||
NPageHeader: typeof import('naive-ui')['NPageHeader']
|
NPageHeader: typeof import('naive-ui')['NPageHeader']
|
||||||
NProgress: typeof import('naive-ui')['NProgress']
|
NProgress: typeof import('naive-ui')['NProgress']
|
||||||
NResult: typeof import('naive-ui')['NResult']
|
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSelect: typeof import('naive-ui')['NSelect']
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
NSlider: typeof import('naive-ui')['NSlider']
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
|
||||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
NTable: typeof import('naive-ui')['NTable']
|
NTable: typeof import('naive-ui')['NTable']
|
||||||
|
@ -63,11 +132,34 @@ declare module '@vue/runtime-core' {
|
||||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||||
NUpload: typeof import('naive-ui')['NUpload']
|
NUpload: typeof import('naive-ui')['NUpload']
|
||||||
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
||||||
|
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
|
||||||
|
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
|
||||||
|
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
|
||||||
|
RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
|
||||||
|
ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']
|
||||||
|
RomanNumeralConverter: typeof import('./src/tools/roman-numeral-converter/roman-numeral-converter.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default']
|
||||||
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
|
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
|
||||||
SearchBarItem: typeof import('./src/components/SearchBarItem.vue')['default']
|
SearchBarItem: typeof import('./src/components/SearchBarItem.vue')['default']
|
||||||
|
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
|
||||||
|
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
|
||||||
|
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
|
||||||
|
SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default']
|
||||||
|
TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default']
|
||||||
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
|
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
|
||||||
|
TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default']
|
||||||
|
TextToNatoAlphabet: typeof import('./src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue')['default']
|
||||||
|
TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default']
|
||||||
|
'TokenGenerator.tool': typeof import('./src/tools/token-generator/token-generator.tool.vue')['default']
|
||||||
|
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
|
||||||
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
|
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
|
||||||
|
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
|
||||||
|
UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default']
|
||||||
|
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']
|
||||||
|
UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default']
|
||||||
|
UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
|
||||||
|
YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
17
package.json
17
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "it-tools",
|
"name": "it-tools",
|
||||||
"version": "2023.4.14-dbad773",
|
"version": "2023.5.14-77f2efc",
|
||||||
"description": "Collection of handy online tools for developers, with great UX. ",
|
"description": "Collection of handy online tools for developers, with great UX. ",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"productivity",
|
"productivity",
|
||||||
|
@ -41,13 +41,14 @@
|
||||||
"@tiptap/vue-3": "2.0.0-beta.220",
|
"@tiptap/vue-3": "2.0.0-beta.220",
|
||||||
"@vicons/material": "^0.12.0",
|
"@vicons/material": "^0.12.0",
|
||||||
"@vicons/tabler": "^0.12.0",
|
"@vicons/tabler": "^0.12.0",
|
||||||
"@vueuse/core": "^8.9.4",
|
"@vueuse/core": "^10.1.2",
|
||||||
"@vueuse/head": "^0.7.13",
|
"@vueuse/head": "^0.7.13",
|
||||||
"@vueuse/router": "^9.13.0",
|
"@vueuse/router": "^9.13.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"change-case": "^4.1.2",
|
"change-case": "^4.1.2",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
"composerize-ts": "^0.6.2",
|
"composerize-ts": "^0.6.2",
|
||||||
|
"country-code-lookup": "^0.0.23",
|
||||||
"cron-validator": "^1.3.1",
|
"cron-validator": "^1.3.1",
|
||||||
"cronstrue": "^2.26.0",
|
"cronstrue": "^2.26.0",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
|
@ -57,6 +58,7 @@
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
|
"libphonenumber-js": "^1.10.28",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mathjs": "^10.6.4",
|
"mathjs": "^10.6.4",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
|
@ -77,6 +79,8 @@
|
||||||
"yaml": "^2.2.1"
|
"yaml": "^2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^0.39.3",
|
||||||
|
"@iconify-json/mdi": "^1.1.50",
|
||||||
"@playwright/test": "^1.32.3",
|
"@playwright/test": "^1.32.3",
|
||||||
"@rushstack/eslint-patch": "^1.2.0",
|
"@rushstack/eslint-patch": "^1.2.0",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
|
@ -96,17 +100,13 @@
|
||||||
"@unocss/eslint-config": "^0.50.8",
|
"@unocss/eslint-config": "^0.50.8",
|
||||||
"@vitejs/plugin-vue": "^2.3.4",
|
"@vitejs/plugin-vue": "^2.3.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
||||||
"@vue/eslint-config-prettier": "^7.1.0",
|
"@vue/compiler-sfc": "^3.2.47",
|
||||||
"@vue/eslint-config-typescript": "^10.0.0",
|
"@vue/runtime-core": "^3.2.47",
|
||||||
"@vue/test-utils": "^2.3.2",
|
"@vue/test-utils": "^2.3.2",
|
||||||
"@vue/tsconfig": "^0.1.3",
|
"@vue/tsconfig": "^0.1.3",
|
||||||
"c8": "^7.13.0",
|
"c8": "^7.13.0",
|
||||||
"consola": "^3.0.2",
|
"consola": "^3.0.2",
|
||||||
"eslint": "^8.38.0",
|
"eslint": "^8.38.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
|
||||||
"eslint-import-resolver-typescript": "^3.5.5",
|
|
||||||
"eslint-plugin-import": "^2.27.5",
|
|
||||||
"eslint-plugin-vue": "^8.7.1",
|
|
||||||
"jsdom": "^19.0.0",
|
"jsdom": "^19.0.0",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"prettier": "^2.8.7",
|
"prettier": "^2.8.7",
|
||||||
|
@ -114,6 +114,7 @@
|
||||||
"typescript": "~4.5.5",
|
"typescript": "~4.5.5",
|
||||||
"unocss": "^0.50.8",
|
"unocss": "^0.50.8",
|
||||||
"unplugin-auto-import": "^0.15.2",
|
"unplugin-auto-import": "^0.15.2",
|
||||||
|
"unplugin-icons": "^0.16.1",
|
||||||
"unplugin-vue-components": "^0.24.1",
|
"unplugin-vue-components": "^0.24.1",
|
||||||
"vite": "^2.9.15",
|
"vite": "^2.9.15",
|
||||||
"vite-plugin-md": "^0.12.4",
|
"vite-plugin-md": "^0.12.4",
|
||||||
|
|
997
pnpm-lock.yaml
generated
997
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -29,9 +29,9 @@ createToolFile(
|
||||||
`${toolName}.vue`,
|
`${toolName}.vue`,
|
||||||
`
|
`
|
||||||
<template>
|
<template>
|
||||||
<n-card>
|
<div>
|
||||||
Lorem ipsum
|
Lorem ipsum
|
||||||
</n-card>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
16
src/App.vue
16
src/App.vue
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRoute, RouterView } from 'vue-router';
|
import { RouterView, useRoute } from 'vue-router';
|
||||||
import { darkTheme, NGlobalStyle, NMessageProvider, NNotificationProvider } from 'naive-ui';
|
import { NGlobalStyle, NMessageProvider, NNotificationProvider, darkTheme } from 'naive-ui';
|
||||||
import { darkThemeOverrides, lightThemeOverrides } from './themes';
|
import { darkThemeOverrides, lightThemeOverrides } from './themes';
|
||||||
import { layouts } from './layouts';
|
import { layouts } from './layouts';
|
||||||
import { useStyleStore } from './stores/style.store';
|
import { useStyleStore } from './stores/style.store';
|
||||||
|
@ -16,14 +16,14 @@ const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrid
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-config-provider :theme="theme" :theme-overrides="themeOverrides">
|
<n-config-provider :theme="theme" :theme-overrides="themeOverrides">
|
||||||
<n-global-style />
|
<NGlobalStyle />
|
||||||
<n-message-provider placement="bottom">
|
<NMessageProvider placement="bottom">
|
||||||
<n-notification-provider placement="bottom-right">
|
<NNotificationProvider placement="bottom-right">
|
||||||
<component :is="layout">
|
<component :is="layout">
|
||||||
<router-view />
|
<RouterView />
|
||||||
</component>
|
</component>
|
||||||
</n-notification-provider>
|
</NNotificationProvider>
|
||||||
</n-message-provider>
|
</NMessageProvider>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,51 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ChevronRight } from '@vicons/tabler';
|
||||||
|
import { useStorage } from '@vueuse/core';
|
||||||
|
import { useThemeVars } from 'naive-ui';
|
||||||
|
import { computed, h, toRefs } from 'vue';
|
||||||
|
import { RouterLink, useRoute } from 'vue-router';
|
||||||
|
import MenuIconItem from './MenuIconItem.vue';
|
||||||
|
import type { Tool, ToolCategory } from '@/tools/tools.types';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] });
|
||||||
|
const { toolsByCategory } = toRefs(props);
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const makeLabel = (tool: Tool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name });
|
||||||
|
const makeIcon = (tool: Tool) => () => h(MenuIconItem, { tool });
|
||||||
|
|
||||||
|
const collapsedCategories = useStorage<Record<string, boolean>>(
|
||||||
|
'menu-tool-option:collapsed-categories',
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
serializer: {
|
||||||
|
read: v => (v ? JSON.parse(v) : null),
|
||||||
|
write: v => JSON.stringify(v),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggleCategoryCollapse({ name }: { name: string }) {
|
||||||
|
collapsedCategories.value[name] = !collapsedCategories.value[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuOptions = computed(() =>
|
||||||
|
toolsByCategory.value.map(({ name, components }) => ({
|
||||||
|
name,
|
||||||
|
isCollapsed: collapsedCategories.value[name],
|
||||||
|
tools: components.map(tool => ({
|
||||||
|
label: makeLabel(tool),
|
||||||
|
icon: makeIcon(tool),
|
||||||
|
key: tool.name,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const themeVars = useThemeVars();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name">
|
<div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name">
|
||||||
<n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })">
|
<n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })">
|
||||||
|
@ -14,7 +62,7 @@
|
||||||
|
|
||||||
<n-menu
|
<n-menu
|
||||||
class="menu"
|
class="menu"
|
||||||
:value="(route.name as string)"
|
:value="route.name as string"
|
||||||
:collapsed-width="64"
|
:collapsed-width="64"
|
||||||
:collapsed-icon-size="22"
|
:collapsed-icon-size="22"
|
||||||
:options="tools"
|
:options="tools"
|
||||||
|
@ -26,54 +74,6 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Tool, ToolCategory } from '@/tools/tools.types';
|
|
||||||
import { ChevronRight } from '@vicons/tabler';
|
|
||||||
import { useStorage } from '@vueuse/core';
|
|
||||||
import { useThemeVars } from 'naive-ui';
|
|
||||||
import { toRefs, computed, h } from 'vue';
|
|
||||||
import { RouterLink, useRoute } from 'vue-router';
|
|
||||||
import MenuIconItem from './MenuIconItem.vue';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] });
|
|
||||||
const { toolsByCategory } = toRefs(props);
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const makeLabel = (tool: Tool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name });
|
|
||||||
const makeIcon = (tool: Tool) => () => h(MenuIconItem, { tool });
|
|
||||||
|
|
||||||
const collapsedCategories = useStorage<Record<string, boolean>>(
|
|
||||||
'menu-tool-option:collapsed-categories',
|
|
||||||
{},
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
deep: true,
|
|
||||||
serializer: {
|
|
||||||
read: (v) => (v ? JSON.parse(v) : null),
|
|
||||||
write: (v) => JSON.stringify(v),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function toggleCategoryCollapse({ name }: { name: string }) {
|
|
||||||
collapsedCategories.value[name] = !collapsedCategories.value[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
const menuOptions = computed(() =>
|
|
||||||
toolsByCategory.value.map(({ name, components }) => ({
|
|
||||||
name: name,
|
|
||||||
isCollapsed: collapsedCategories.value[name],
|
|
||||||
tools: components.map((tool) => ({
|
|
||||||
label: makeLabel(tool),
|
|
||||||
icon: makeIcon(tool),
|
|
||||||
key: tool.name,
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const themeVars = useThemeVars();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.category-name {
|
.category-name {
|
||||||
font-size: 0.93em;
|
font-size: 0.93em;
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type Component, toRefs } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ icon: Component; title: string }>();
|
||||||
|
const { icon, title } = toRefs(props);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card class="colored-card">
|
<c-card class="colored-card">
|
||||||
<n-space justify="space-between" align="center">
|
|
||||||
<n-icon class="icon" size="40" :component="icon" />
|
<n-icon class="icon" size="40" :component="icon" />
|
||||||
</n-space>
|
|
||||||
<n-h3 class="title">
|
<n-h3 class="title">
|
||||||
<n-ellipsis>{{ title }}</n-ellipsis>
|
<n-ellipsis>{{ title }}</n-ellipsis>
|
||||||
</n-h3>
|
</n-h3>
|
||||||
|
@ -12,16 +17,9 @@
|
||||||
<slot />
|
<slot />
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</c-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { toRefs, type Component } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps<{ icon: Component; title: string }>();
|
|
||||||
const { icon, title } = toRefs(props);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.colored-card {
|
.colored-card {
|
||||||
background: rgb(37, 99, 108);
|
background: rgb(37, 99, 108);
|
||||||
|
|
|
@ -1,25 +1,13 @@
|
||||||
<template>
|
|
||||||
<n-tooltip trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button circle quaternary :type="buttonType" :style="{ opacity: isFavorite ? 1 : 0.2 }" @click="toggleFavorite">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="FavoriteFilled" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
{{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }}
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FavoriteFilled } from '@vicons/material';
|
import { FavoriteFilled } from '@vicons/material';
|
||||||
|
import { computed, toRefs } from 'vue';
|
||||||
import { useToolStore } from '@/tools/tools.store';
|
import { useToolStore } from '@/tools/tools.store';
|
||||||
import type { Tool } from '@/tools/tools.types';
|
import type { Tool } from '@/tools/tools.types';
|
||||||
import { computed, toRefs } from 'vue';
|
|
||||||
|
const props = defineProps<{ tool: Tool }>();
|
||||||
|
|
||||||
const toolStore = useToolStore();
|
const toolStore = useToolStore();
|
||||||
|
|
||||||
const props = defineProps<{ tool: Tool }>();
|
|
||||||
const { tool } = toRefs(props);
|
const { tool } = toRefs(props);
|
||||||
|
|
||||||
const isFavorite = computed(() => toolStore.isToolFavorite({ tool }));
|
const isFavorite = computed(() => toolStore.isToolFavorite({ tool }));
|
||||||
|
@ -36,3 +24,20 @@ function toggleFavorite(event: MouseEvent) {
|
||||||
toolStore.addToolToFavorites({ tool });
|
toolStore.addToolToFavorites({ tool });
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<c-button
|
||||||
|
variant="text"
|
||||||
|
circle
|
||||||
|
:type="buttonType"
|
||||||
|
:style="{ opacity: isFavorite ? 1 : 0.2 }"
|
||||||
|
@click="toggleFavorite"
|
||||||
|
>
|
||||||
|
<n-icon :component="FavoriteFilled" />
|
||||||
|
</c-button>
|
||||||
|
</template>
|
||||||
|
{{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }}
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,36 +1,17 @@
|
||||||
<template>
|
|
||||||
<n-form-item :label="inputLabel" v-bind="validationAttrs">
|
|
||||||
<n-input
|
|
||||||
ref="inputElement"
|
|
||||||
v-model:value="input"
|
|
||||||
:placeholder="inputPlaceholder"
|
|
||||||
type="textarea"
|
|
||||||
rows="20"
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
:input-props="{ 'data-test-id': 'input' }"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="outputLabel">
|
|
||||||
<textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement" />
|
|
||||||
</n-form-item>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useValidation, type UseValidationRule } from '@/composable/validation';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import type { UseValidationRule } from '@/composable/validation';
|
||||||
|
import CInputText from '@/ui/c-input-text/c-input-text.vue';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
transformer?: (v: string) => string;
|
transformer?: (v: string) => string
|
||||||
inputValidationRules?: UseValidationRule<string>[];
|
inputValidationRules?: UseValidationRule<string>[]
|
||||||
inputLabel?: string;
|
inputLabel?: string
|
||||||
inputPlaceholder?: string;
|
inputPlaceholder?: string
|
||||||
inputDefault?: string;
|
inputDefault?: string
|
||||||
outputLabel?: string;
|
outputLabel?: string
|
||||||
outputLanguage?: string;
|
outputLanguage?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
transformer: _.identity,
|
transformer: _.identity,
|
||||||
|
@ -43,15 +24,33 @@ const props = withDefaults(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } =
|
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault }
|
||||||
toRefs(props);
|
= toRefs(props);
|
||||||
|
|
||||||
const inputElement = ref();
|
const inputElement = ref<typeof CInputText>();
|
||||||
|
|
||||||
const input = ref(inputDefault.value);
|
const input = ref(inputDefault.value);
|
||||||
const output = computed(() => transformer.value(input.value));
|
const output = computed(() => transformer.value(input.value));
|
||||||
|
|
||||||
const { attrs: validationAttrs } = useValidation({ source: input, rules: inputValidationRules.value });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<template>
|
||||||
|
<CInputText
|
||||||
|
ref="inputElement"
|
||||||
|
v-model:value="input"
|
||||||
|
:placeholder="inputPlaceholder"
|
||||||
|
:label="inputLabel"
|
||||||
|
rows="20"
|
||||||
|
autosize
|
||||||
|
raw-text
|
||||||
|
multiline
|
||||||
|
test-id="input"
|
||||||
|
:validation-rules="inputValidationRules"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div mb-5px>
|
||||||
|
{{ outputLabel }}
|
||||||
|
</div>
|
||||||
|
<textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,21 +1,5 @@
|
||||||
<template>
|
|
||||||
<n-input v-model:value="value">
|
|
||||||
<template #suffix>
|
|
||||||
<n-tooltip trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button quaternary circle @click="onCopyClicked">
|
|
||||||
<n-icon :component="ContentCopyFilled" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
{{ tooltipText }}
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
|
||||||
</n-input>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel, useClipboard } from '@vueuse/core';
|
import { useClipboard, useVModel } from '@vueuse/core';
|
||||||
import { ContentCopyFilled } from '@vicons/material';
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{ value: string }>();
|
const props = defineProps<{ value: string }>();
|
||||||
|
@ -36,8 +20,17 @@ function onCopyClicked() {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<template>
|
||||||
::v-deep(.n-input-wrapper) {
|
<c-input-text v-model:value="value">
|
||||||
padding-right: 5px;
|
<template #suffix>
|
||||||
}
|
<n-tooltip trigger="hover">
|
||||||
</style>
|
<template #trigger>
|
||||||
|
<c-button circle variant="text" size="small" @click="onCopyClicked">
|
||||||
|
<icon-mdi-content-copy />
|
||||||
|
</c-button>
|
||||||
|
</template>
|
||||||
|
{{ tooltipText }}
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
</c-input-text>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
<template>
|
|
||||||
<div class="menu-icon-item">
|
|
||||||
<n-icon :component="tool.icon" />
|
|
||||||
<div v-if="tool.isNew" class="badge"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Tool } from '@/tools/tools.types';
|
|
||||||
import { useThemeVars } from 'naive-ui';
|
import { useThemeVars } from 'naive-ui';
|
||||||
import { toRefs } from 'vue';
|
import { toRefs } from 'vue';
|
||||||
|
import type { Tool } from '@/tools/tools.types';
|
||||||
|
|
||||||
const props = defineProps<{ tool: Tool }>();
|
const props = defineProps<{ tool: Tool }>();
|
||||||
const { tool } = toRefs(props);
|
const { tool } = toRefs(props);
|
||||||
|
@ -16,6 +9,13 @@ const { tool } = toRefs(props);
|
||||||
const theme = useThemeVars();
|
const theme = useThemeVars();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="menu-icon-item">
|
||||||
|
<n-icon :component="tool.icon" />
|
||||||
|
<div v-if="tool.isNew" class="badge" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.menu-icon-item {
|
.menu-icon-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, toRefs } from 'vue';
|
||||||
|
import { useStyleStore } from '@/stores/style.store';
|
||||||
|
|
||||||
|
const styleStore = useStyleStore();
|
||||||
|
const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore);
|
||||||
|
const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static'));
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-layout has-sider>
|
<n-layout has-sider>
|
||||||
<n-layout-sider
|
<n-layout-sider
|
||||||
|
@ -19,15 +28,6 @@
|
||||||
</n-layout>
|
</n-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useStyleStore } from '@/stores/style.store';
|
|
||||||
import { toRefs, computed } from 'vue';
|
|
||||||
|
|
||||||
const styleStore = useStyleStore();
|
|
||||||
const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore);
|
|
||||||
const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static'));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.overlay {
|
.overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -1,71 +1,74 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler';
|
||||||
|
import { toRefs } from 'vue';
|
||||||
|
import { useStyleStore } from '@/stores/style.store';
|
||||||
|
import { useThemeStore } from '@/ui/theme/theme.store';
|
||||||
|
|
||||||
|
const styleStore = useStyleStore();
|
||||||
|
const { isDarkTheme } = toRefs(styleStore);
|
||||||
|
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
function toggleDarkTheme() {
|
||||||
|
isDarkTheme.value = !isDarkTheme.value;
|
||||||
|
|
||||||
|
themeStore.toggleTheme();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-tooltip trigger="hover">
|
<n-tooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button
|
<c-button
|
||||||
size="large"
|
|
||||||
circle
|
circle
|
||||||
quaternary
|
variant="text"
|
||||||
tag="a"
|
|
||||||
href="https://github.com/CorentinTh/it-tools"
|
href="https://github.com/CorentinTh/it-tools"
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
aria-label="IT-Tools' GitHub repository"
|
aria-label="IT-Tools' GitHub repository"
|
||||||
>
|
>
|
||||||
<n-icon size="25" :component="BrandGithub" />
|
<n-icon size="25" :component="BrandGithub" />
|
||||||
</n-button>
|
</c-button>
|
||||||
</template>
|
</template>
|
||||||
Github repository
|
Github repository
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
|
|
||||||
<n-tooltip trigger="hover">
|
<n-tooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button
|
<c-button
|
||||||
size="large"
|
|
||||||
circle
|
circle
|
||||||
quaternary
|
variant="text"
|
||||||
tag="a"
|
|
||||||
href="https://twitter.com/ittoolsdottech"
|
href="https://twitter.com/ittoolsdottech"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
aria-label="IT Tools' Twitter account"
|
aria-label="IT Tools' Twitter account"
|
||||||
>
|
>
|
||||||
<n-icon size="25" :component="BrandTwitter" />
|
<n-icon size="25" :component="BrandTwitter" />
|
||||||
</n-button>
|
</c-button>
|
||||||
</template>
|
</template>
|
||||||
IT Tools' Twitter account
|
IT Tools' Twitter account
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
|
|
||||||
<router-link to="/about" #="{ navigate, href }" custom>
|
|
||||||
<n-tooltip trigger="hover">
|
<n-tooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button tag="a" :href="href" circle quaternary size="large" aria-label="About" @click="navigate">
|
<c-button circle variant="text" to="/about" aria-label="About">
|
||||||
<n-icon size="25" :component="InfoCircle" />
|
<n-icon size="25" :component="InfoCircle" />
|
||||||
</n-button>
|
</c-button>
|
||||||
</template>
|
</template>
|
||||||
About
|
About
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</router-link>
|
|
||||||
<n-tooltip trigger="hover">
|
<n-tooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button size="large" circle quaternary aria-label="Toggle dark/light mode" @click="isDarkTheme = !isDarkTheme">
|
<c-button circle variant="text" aria-label="Toggle dark/light mode" @click="toggleDarkTheme">
|
||||||
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
|
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
|
||||||
<n-icon v-else size="25" :component="Moon" />
|
<n-icon v-else size="25" :component="Moon" />
|
||||||
</n-button>
|
</c-button>
|
||||||
</template>
|
</template>
|
||||||
<span v-if="isDarkTheme">Light mode</span>
|
<span v-if="isDarkTheme">Light mode</span>
|
||||||
<span v-else>Dark mode</span>
|
<span v-else>Dark mode</span>
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useStyleStore } from '@/stores/style.store';
|
|
||||||
import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler';
|
|
||||||
import { toRefs } from 'vue';
|
|
||||||
|
|
||||||
const styleStore = useStyleStore();
|
|
||||||
const { isDarkTheme } = toRefs(styleStore);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.n-button {
|
.n-button {
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useFuzzySearch } from '@/composable/fuzzySearch';
|
|
||||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
|
||||||
import { tools } from '@/tools';
|
|
||||||
import type { Tool } from '@/tools/tools.types';
|
|
||||||
import { SearchRound } from '@vicons/material';
|
import { SearchRound } from '@vicons/material';
|
||||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
import { useMagicKeys, whenever } from '@vueuse/core';
|
||||||
import type { NInput } from 'naive-ui';
|
import { NInput } from 'naive-ui';
|
||||||
import { computed, h, ref } from 'vue';
|
import { computed, h, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import SearchBarItem from './SearchBarItem.vue';
|
import SearchBarItem from './SearchBarItem.vue';
|
||||||
|
import type { Tool } from '@/tools/tools.types';
|
||||||
|
import { tools } from '@/tools';
|
||||||
|
import { useTracker } from '@/modules/tracker/tracker.services';
|
||||||
|
import { useFuzzySearch } from '@/composable/fuzzySearch';
|
||||||
|
|
||||||
const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool });
|
const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool });
|
||||||
|
|
||||||
|
@ -20,6 +20,12 @@ const inputEl = ref<HTMLElement>();
|
||||||
const displayDropDown = ref(true);
|
const displayDropDown = ref(true);
|
||||||
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
|
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
|
||||||
|
|
||||||
|
const { searchResult } = useFuzzySearch({
|
||||||
|
search: queryString,
|
||||||
|
data: tools,
|
||||||
|
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
|
||||||
|
});
|
||||||
|
|
||||||
const options = computed(() => {
|
const options = computed(() => {
|
||||||
if (queryString.value === '') {
|
if (queryString.value === '') {
|
||||||
return tools.map(toolToOption);
|
return tools.map(toolToOption);
|
||||||
|
@ -28,12 +34,6 @@ const options = computed(() => {
|
||||||
return searchResult.value.map(toolToOption);
|
return searchResult.value.map(toolToOption);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { searchResult } = useFuzzySearch({
|
|
||||||
search: queryString,
|
|
||||||
data: tools,
|
|
||||||
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
|
|
||||||
});
|
|
||||||
|
|
||||||
const keys = useMagicKeys({
|
const keys = useMagicKeys({
|
||||||
passive: false,
|
passive: false,
|
||||||
onEventFired(e) {
|
onEventFired(e) {
|
||||||
|
@ -81,15 +81,15 @@ function onFocus() {
|
||||||
<n-auto-complete
|
<n-auto-complete
|
||||||
v-model:value="queryString"
|
v-model:value="queryString"
|
||||||
:options="options"
|
:options="options"
|
||||||
:on-select="(value) => onSelect(String(value))"
|
:on-select="(value: string | number) => onSelect(String(value))"
|
||||||
:render-label="renderOption"
|
:render-label="renderOption"
|
||||||
:default-value="'aa'"
|
default-value="aa"
|
||||||
:get-show="() => displayDropDown"
|
:get-show="() => displayDropDown"
|
||||||
:on-focus="onFocus"
|
:on-focus="onFocus"
|
||||||
@update:value="() => (displayDropDown = true)"
|
@update:value="() => (displayDropDown = true)"
|
||||||
>
|
>
|
||||||
<template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
|
<template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
|
||||||
<n-input
|
<NInput
|
||||||
ref="inputEl"
|
ref="inputEl"
|
||||||
round
|
round
|
||||||
clearable
|
clearable
|
||||||
|
@ -103,7 +103,7 @@ function onFocus() {
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<n-icon :component="SearchRound" />
|
<n-icon :component="SearchRound" />
|
||||||
</template>
|
</template>
|
||||||
</n-input>
|
</NInput>
|
||||||
</template>
|
</template>
|
||||||
</n-auto-complete>
|
</n-auto-complete>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Tool } from '@/tools/tools.types';
|
|
||||||
import { toRefs } from 'vue';
|
import { toRefs } from 'vue';
|
||||||
|
import type { Tool } from '@/tools/tools.types';
|
||||||
|
|
||||||
const props = defineProps<{ tool: Tool }>();
|
const props = defineProps<{ tool: Tool }>();
|
||||||
const { tool } = toRefs(props);
|
const { tool } = toRefs(props);
|
||||||
|
@ -11,8 +11,12 @@ const { tool } = toRefs(props);
|
||||||
<n-icon class="icon" :component="tool.icon" />
|
<n-icon class="icon" :component="tool.icon" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="name">{{ tool.name }}</div>
|
<div class="name">
|
||||||
<div class="description">{{ tool.description }}</div>
|
{{ tool.name }}
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
{{ tool.description }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,23 +1,14 @@
|
||||||
<template>
|
|
||||||
<n-tooltip trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<span class="ip" @click="handleClick">{{ ip }}</span>
|
|
||||||
</template>
|
|
||||||
{{ tooltipText }}
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useClipboard } from '@vueuse/core';
|
import { useClipboard } from '@vueuse/core';
|
||||||
import { ref, toRefs } from 'vue';
|
import { ref, toRefs } from 'vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ ip?: string }>(), { ip: '' });
|
const props = withDefaults(defineProps<{ value?: string }>(), { value: '' });
|
||||||
const { ip } = toRefs(props);
|
const { value } = toRefs(props);
|
||||||
|
|
||||||
const initialText = 'Copy to clipboard';
|
const initialText = 'Copy to clipboard';
|
||||||
const tooltipText = ref(initialText);
|
const tooltipText = ref(initialText);
|
||||||
|
|
||||||
const { copy } = useClipboard({ source: ip });
|
const { copy } = useClipboard({ source: value });
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
copy();
|
copy();
|
||||||
|
@ -27,9 +18,18 @@ function handleClick() {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<span class="value" @click="handleClick">{{ value }}</span>
|
||||||
|
</template>
|
||||||
|
{{ tooltipText }}
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.ip {
|
.value {
|
||||||
font-family: monospace;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,32 +1,3 @@
|
||||||
<template>
|
|
||||||
<div style="overflow-x: hidden; width: 100%">
|
|
||||||
<n-card class="result-card">
|
|
||||||
<n-scrollbar
|
|
||||||
x-scrollable
|
|
||||||
trigger="none"
|
|
||||||
:style="height ? `min-height: ${height - 40 /* card padding */ + 10 /* negative margin compensation */}px` : ''"
|
|
||||||
>
|
|
||||||
<n-config-provider :hljs="hljs">
|
|
||||||
<n-code :code="value" :language="language" :trim="false" data-test-id="area-content" />
|
|
||||||
</n-config-provider>
|
|
||||||
</n-scrollbar>
|
|
||||||
<n-tooltip v-if="value" trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<div class="copy-button" :class="[copyPlacement]">
|
|
||||||
<n-button circle secondary size="large" @click="onCopyClicked">
|
|
||||||
<n-icon size="22" :component="Copy" />
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<span>{{ tooltipText }}</span>
|
|
||||||
</n-tooltip>
|
|
||||||
</n-card>
|
|
||||||
<n-space v-if="copyPlacement === 'outside'" justify="center" mt-4>
|
|
||||||
<n-button secondary @click="onCopyClicked"> {{ tooltipText }} </n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Copy } from '@vicons/tabler';
|
import { Copy } from '@vicons/tabler';
|
||||||
import { useClipboard, useElementSize } from '@vueuse/core';
|
import { useClipboard, useElementSize } from '@vueuse/core';
|
||||||
|
@ -37,18 +8,13 @@ import xmlHljs from 'highlight.js/lib/languages/xml';
|
||||||
import yamlHljs from 'highlight.js/lib/languages/yaml';
|
import yamlHljs from 'highlight.js/lib/languages/yaml';
|
||||||
import { ref, toRefs } from 'vue';
|
import { ref, toRefs } from 'vue';
|
||||||
|
|
||||||
hljs.registerLanguage('sql', sqlHljs);
|
|
||||||
hljs.registerLanguage('json', jsonHljs);
|
|
||||||
hljs.registerLanguage('html', xmlHljs);
|
|
||||||
hljs.registerLanguage('yaml', yamlHljs);
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
value: string;
|
value: string
|
||||||
followHeightOf?: HTMLElement | null;
|
followHeightOf?: HTMLElement | null
|
||||||
language?: string;
|
language?: string
|
||||||
copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none';
|
copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none'
|
||||||
copyMessage?: string;
|
copyMessage?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
followHeightOf: null,
|
followHeightOf: null,
|
||||||
|
@ -57,8 +23,13 @@ const props = withDefaults(
|
||||||
copyMessage: 'Copy to clipboard',
|
copyMessage: 'Copy to clipboard',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
hljs.registerLanguage('sql', sqlHljs);
|
||||||
|
hljs.registerLanguage('json', jsonHljs);
|
||||||
|
hljs.registerLanguage('html', xmlHljs);
|
||||||
|
hljs.registerLanguage('yaml', yamlHljs);
|
||||||
|
|
||||||
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
|
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
|
||||||
const { height } = followHeightOf ? useElementSize(followHeightOf) : { height: ref(null) };
|
const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) };
|
||||||
|
|
||||||
const { copy } = useClipboard({ source: value });
|
const { copy } = useClipboard({ source: value });
|
||||||
const tooltipText = ref(copyMessage.value);
|
const tooltipText = ref(copyMessage.value);
|
||||||
|
@ -73,6 +44,37 @@ function onCopyClicked() {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="overflow-x: hidden; width: 100%">
|
||||||
|
<c-card class="result-card">
|
||||||
|
<n-scrollbar
|
||||||
|
x-scrollable
|
||||||
|
trigger="none"
|
||||||
|
:style="height ? `min-height: ${height - 40 /* card padding */ + 10 /* negative margin compensation */}px` : ''"
|
||||||
|
>
|
||||||
|
<n-config-provider :hljs="hljs">
|
||||||
|
<n-code :code="value" :language="language" :trim="false" data-test-id="area-content" />
|
||||||
|
</n-config-provider>
|
||||||
|
</n-scrollbar>
|
||||||
|
<n-tooltip v-if="value" trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<div class="copy-button" :class="[copyPlacement]">
|
||||||
|
<c-button circle important:h-10 important:w-10 @click="onCopyClicked">
|
||||||
|
<n-icon size="22" :component="Copy" />
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span>{{ tooltipText }}</span>
|
||||||
|
</n-tooltip>
|
||||||
|
</c-card>
|
||||||
|
<div v-if="copyPlacement === 'outside'" mt-4 flex justify-center>
|
||||||
|
<c-button @click="onCopyClicked">
|
||||||
|
{{ tooltipText }}
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
::v-deep(.n-scrollbar) {
|
::v-deep(.n-scrollbar) {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useThemeVars } from 'naive-ui';
|
||||||
|
import { toRefs } from 'vue';
|
||||||
|
import FavoriteButton from './FavoriteButton.vue';
|
||||||
|
import { useAppTheme } from '@/ui/theme/themes';
|
||||||
|
import type { Tool } from '@/tools/tools.types';
|
||||||
|
|
||||||
|
const props = defineProps<{ tool: Tool & { category: string } }>();
|
||||||
|
const { tool } = toRefs(props);
|
||||||
|
const theme = useThemeVars();
|
||||||
|
|
||||||
|
const appTheme = useAppTheme();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-link :to="tool.path">
|
<router-link :to="tool.path">
|
||||||
<n-card class="tool-card">
|
<c-card class="tool-card">
|
||||||
<n-space justify="space-between" align="center">
|
<div flex items-center justify-between>
|
||||||
<n-icon class="icon" size="40" :component="tool.icon" />
|
<n-icon class="icon" size="40" :component="tool.icon" />
|
||||||
<n-space align="center">
|
<div flex items-center gap-8px>
|
||||||
<n-tag
|
<n-tag
|
||||||
v-if="tool.isNew"
|
v-if="tool.isNew"
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -16,9 +30,9 @@
|
||||||
New
|
New
|
||||||
</n-tag>
|
</n-tag>
|
||||||
|
|
||||||
<favorite-button :tool="tool" />
|
<FavoriteButton :tool="tool" />
|
||||||
</n-space>
|
</div>
|
||||||
</n-space>
|
</div>
|
||||||
<n-h3 class="title">
|
<n-h3 class="title">
|
||||||
<n-ellipsis>{{ tool.name }}</n-ellipsis>
|
<n-ellipsis>{{ tool.name }}</n-ellipsis>
|
||||||
</n-h3>
|
</n-h3>
|
||||||
|
@ -26,32 +40,25 @@
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px">
|
<n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px">
|
||||||
{{ tool.description }}
|
{{ tool.description }}
|
||||||
<br />
|
<br>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</c-card>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Tool } from '@/tools/tools.types';
|
|
||||||
import { useThemeVars } from 'naive-ui';
|
|
||||||
import { toRefs } from 'vue';
|
|
||||||
import FavoriteButton from './FavoriteButton.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{ tool: Tool & { category: string } }>();
|
|
||||||
const { tool } = toRefs(props);
|
|
||||||
const theme = useThemeVars();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-card {
|
.tool-card {
|
||||||
|
transition: border-color ease 0.5s;
|
||||||
|
border-width: 2px !important;
|
||||||
|
color: transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--n-color-target);
|
border-color: v-bind('appTheme.primary.colorHover');
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
@ -11,7 +11,8 @@ function computedRefreshable<T>(getter: () => T, { throttle }: { throttle?: numb
|
||||||
|
|
||||||
if (throttle) {
|
if (throttle) {
|
||||||
watchThrottled(getter, update, { throttle });
|
watchThrottled(getter, update, { throttle });
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
watch(getter, update);
|
watch(getter, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { useClipboard } from '@vueuse/core';
|
import { type MaybeRef, get, useClipboard } from '@vueuse/core';
|
||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
import type { Ref } from 'vue';
|
|
||||||
|
|
||||||
export function useCopy({ source, text = 'Copied to the clipboard' }: { source: Ref; text?: string }) {
|
export function useCopy({ source, text = 'Copied to the clipboard' }: { source: MaybeRef<unknown>; text?: string }) {
|
||||||
const { copy } = useClipboard({ source });
|
const { copy } = useClipboard({ source: computed(() => String(get(source))) });
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -5,8 +5,8 @@ function getFileExtensionFromBase64({
|
||||||
base64String,
|
base64String,
|
||||||
defaultExtension = 'txt',
|
defaultExtension = 'txt',
|
||||||
}: {
|
}: {
|
||||||
base64String: string;
|
base64String: string
|
||||||
defaultExtension?: string;
|
defaultExtension?: string
|
||||||
}) {
|
}) {
|
||||||
const hasMimeType = base64String.match(/data:(.*?);base64/i);
|
const hasMimeType = base64String.match(/data:(.*?);base64/i);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { get, type MaybeRef } from '@vueuse/core';
|
import { type MaybeRef, get } from '@vueuse/core';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@ function useFuzzySearch<Data>({
|
||||||
data,
|
data,
|
||||||
options = {},
|
options = {},
|
||||||
}: {
|
}: {
|
||||||
search: MaybeRef<string>;
|
search: MaybeRef<string>
|
||||||
data: Data[];
|
data: Data[]
|
||||||
options?: Fuse.IFuseOptions<Data>;
|
options?: Fuse.IFuseOptions<Data>
|
||||||
}) {
|
}) {
|
||||||
const fuse = new Fuse(data, options);
|
const fuse = new Fuse(data, options);
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue:
|
||||||
|
|
||||||
return computed<T>({
|
return computed<T>({
|
||||||
get() {
|
get() {
|
||||||
return transformer.fromQuery(proxy.value) as T;
|
return transformer.fromQuery(proxy.value) as unknown as T;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
proxy.value = transformer.toQuery(value as never);
|
proxy.value = transformer.toQuery(value as never);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { isFalsyOrHasThrown } from './validation';
|
import { isFalsyOrHasThrown } from './validation';
|
||||||
|
|
||||||
|
@ -11,7 +10,7 @@ describe('useValidation', () => {
|
||||||
expect(isFalsyOrHasThrown(() => {})).toBe(true);
|
expect(isFalsyOrHasThrown(() => {})).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
isFalsyOrHasThrown(() => {
|
isFalsyOrHasThrown(() => {
|
||||||
throw new Error();
|
throw new Error('message');
|
||||||
}),
|
}),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,44 +1,48 @@
|
||||||
|
import { type MaybeRef, get } from '@vueuse/core';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { reactive, watch, type Ref } from 'vue';
|
import { type Ref, reactive, watch } from 'vue';
|
||||||
|
|
||||||
type ValidatorReturnType = unknown;
|
type ValidatorReturnType = unknown;
|
||||||
|
|
||||||
export interface UseValidationRule<T> {
|
export interface UseValidationRule<T> {
|
||||||
validator: (value: T) => ValidatorReturnType;
|
validator: (value: T) => ValidatorReturnType
|
||||||
message: string;
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
|
export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
|
||||||
try {
|
try {
|
||||||
const returnValue = cb();
|
const returnValue = cb();
|
||||||
|
|
||||||
if (_.isNil(returnValue)) return true;
|
if (_.isNil(returnValue)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return returnValue === false;
|
return returnValue === false;
|
||||||
} catch (_) {
|
}
|
||||||
|
catch (_) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ValidationAttrs = {
|
export interface ValidationAttrs {
|
||||||
feedback: string;
|
feedback: string
|
||||||
validationStatus: string | undefined;
|
validationStatus: string | undefined
|
||||||
};
|
}
|
||||||
|
|
||||||
export function useValidation<T>({
|
export function useValidation<T>({
|
||||||
source,
|
source,
|
||||||
rules,
|
rules,
|
||||||
watch: watchRefs = [],
|
watch: watchRefs = [],
|
||||||
}: {
|
}: {
|
||||||
source: Ref<T>;
|
source: Ref<T>
|
||||||
rules: UseValidationRule<T>[];
|
rules: MaybeRef<UseValidationRule<T>[]>
|
||||||
watch?: Ref<unknown>[];
|
watch?: Ref<unknown>[]
|
||||||
}) {
|
}) {
|
||||||
const state = reactive<{
|
const state = reactive<{
|
||||||
message: string;
|
message: string
|
||||||
status: undefined | 'error';
|
status: undefined | 'error'
|
||||||
isValid: boolean;
|
isValid: boolean
|
||||||
attrs: ValidationAttrs;
|
attrs: ValidationAttrs
|
||||||
}>({
|
}>({
|
||||||
message: '',
|
message: '',
|
||||||
status: undefined,
|
status: undefined,
|
||||||
|
@ -55,7 +59,7 @@ export function useValidation<T>({
|
||||||
state.message = '';
|
state.message = '';
|
||||||
state.status = undefined;
|
state.status = undefined;
|
||||||
|
|
||||||
for (const rule of rules) {
|
for (const rule of get(rules)) {
|
||||||
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
|
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
|
||||||
state.message = rule.message;
|
state.message = rule.message;
|
||||||
state.status = 'error';
|
state.status = 'error';
|
||||||
|
|
|
@ -23,9 +23,9 @@ export const config = figue({
|
||||||
env: {
|
env: {
|
||||||
doc: 'Application current env',
|
doc: 'Application current env',
|
||||||
format: 'enum',
|
format: 'enum',
|
||||||
values: ['production', 'development', 'test'],
|
values: ['production', 'development', 'preview', 'test'],
|
||||||
default: 'development',
|
default: 'development',
|
||||||
env: 'MODE',
|
env: 'VITE_VERCEL_ENV',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plausible: {
|
plausible: {
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
import { NIcon, useThemeVars } from 'naive-ui';
|
import { NIcon, useThemeVars } from 'naive-ui';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { RouterLink } from 'vue-router';
|
import { RouterLink } from 'vue-router';
|
||||||
import { Heart, Menu2, Home2 } from '@vicons/tabler';
|
import { Heart, Home2, Menu2 } from '@vicons/tabler';
|
||||||
|
import SearchBar from '../components/SearchBar.vue';
|
||||||
|
import HeroGradient from '../assets/hero-gradient.svg?component';
|
||||||
|
import MenuLayout from '../components/MenuLayout.vue';
|
||||||
|
import NavbarButtons from '../components/NavbarButtons.vue';
|
||||||
import { toolsByCategory } from '@/tools';
|
import { toolsByCategory } from '@/tools';
|
||||||
import { useStyleStore } from '@/stores/style.store';
|
import { useStyleStore } from '@/stores/style.store';
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
|
@ -10,10 +14,6 @@ import type { ToolCategory } from '@/tools/tools.types';
|
||||||
import { useToolStore } from '@/tools/tools.store';
|
import { useToolStore } from '@/tools/tools.store';
|
||||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
import { useTracker } from '@/modules/tracker/tracker.services';
|
||||||
import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue';
|
import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue';
|
||||||
import SearchBar from '../components/SearchBar.vue';
|
|
||||||
import HeroGradient from '../assets/hero-gradient.svg?component';
|
|
||||||
import MenuLayout from '../components/MenuLayout.vue';
|
|
||||||
import NavbarButtons from '../components/NavbarButtons.vue';
|
|
||||||
|
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
const styleStore = useStyleStore();
|
const styleStore = useStyleStore();
|
||||||
|
@ -31,60 +31,53 @@ const tools = computed<ToolCategory[]>(() => [
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<menu-layout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }">
|
<MenuLayout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }">
|
||||||
<template #sider>
|
<template #sider>
|
||||||
<router-link to="/" class="hero-wrapper">
|
<RouterLink to="/" class="hero-wrapper">
|
||||||
<hero-gradient class="gradient" />
|
<HeroGradient class="gradient" />
|
||||||
<div class="text-wrapper">
|
<div class="text-wrapper">
|
||||||
<div class="title">IT - TOOLS</div>
|
<div class="title">
|
||||||
<div class="divider" />
|
IT - TOOLS
|
||||||
<div class="subtitle">Handy tools for developers</div>
|
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
<div class="divider" />
|
||||||
|
<div class="subtitle">
|
||||||
|
Handy tools for developers
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
<div class="sider-content">
|
<div class="sider-content">
|
||||||
<n-space v-if="styleStore.isSmallScreen" justify="center">
|
<div v-if="styleStore.isSmallScreen" flex justify-center>
|
||||||
<navbar-buttons />
|
<NavbarButtons />
|
||||||
</n-space>
|
</div>
|
||||||
|
|
||||||
<collapsible-tool-menu :tools-by-category="tools" />
|
<CollapsibleToolMenu :tools-by-category="tools" />
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div>
|
<div>
|
||||||
IT-Tools
|
IT-Tools
|
||||||
|
|
||||||
<n-button
|
<c-link target="_blank" rel="noopener" :href="`https://github.com/CorentinTh/it-tools/tree/v${version}`">
|
||||||
text
|
|
||||||
tag="a"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
type="primary"
|
|
||||||
depth="3"
|
|
||||||
:href="`https://github.com/CorentinTh/it-tools/tree/v${version}`"
|
|
||||||
>
|
|
||||||
v{{ version }}
|
v{{ version }}
|
||||||
</n-button>
|
</c-link>
|
||||||
|
|
||||||
<template v-if="commitSha && commitSha.length > 0">
|
<template v-if="commitSha && commitSha.length > 0">
|
||||||
-
|
-
|
||||||
<n-button
|
<c-link
|
||||||
text
|
|
||||||
tag="a"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
type="primary"
|
type="primary"
|
||||||
depth="3"
|
|
||||||
:href="`https://github.com/CorentinTh/it-tools/tree/${commitSha}`"
|
:href="`https://github.com/CorentinTh/it-tools/tree/${commitSha}`"
|
||||||
>
|
>
|
||||||
{{ commitSha }}
|
{{ commitSha }}
|
||||||
</n-button>
|
</c-link>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
© {{ new Date().getFullYear() }}
|
© {{ new Date().getFullYear() }}
|
||||||
<n-button text tag="a" target="_blank" rel="noopener" type="primary" href="https://github.com/CorentinTh">
|
<c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh">
|
||||||
Corentin Thomasset
|
Corentin Thomasset
|
||||||
</n-button>
|
</c-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -92,45 +85,33 @@ const tools = computed<ToolCategory[]>(() => [
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="navigation">
|
<div class="navigation">
|
||||||
<n-button
|
<c-button
|
||||||
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
|
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
|
||||||
circle
|
circle
|
||||||
quaternary
|
variant="text"
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
|
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
|
||||||
>
|
>
|
||||||
<n-icon size="25" :component="Menu2" />
|
<NIcon size="25" :component="Menu2" />
|
||||||
</n-button>
|
</c-button>
|
||||||
|
|
||||||
<router-link to="/" #="{ navigate, href }" custom>
|
|
||||||
<n-tooltip trigger="hover">
|
<n-tooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button
|
<c-button to="/" circle variant="text" aria-label="Home">
|
||||||
tag="a"
|
<NIcon size="25" :component="Home2" />
|
||||||
:href="href"
|
</c-button>
|
||||||
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
|
|
||||||
circle
|
|
||||||
quaternary
|
|
||||||
aria-label="Home"
|
|
||||||
@click="navigate"
|
|
||||||
>
|
|
||||||
<n-icon size="25" :component="Home2" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
</template>
|
||||||
Home
|
Home
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<search-bar />
|
<SearchBar />
|
||||||
|
|
||||||
<navbar-buttons v-if="!styleStore.isSmallScreen" />
|
<NavbarButtons v-if="!styleStore.isSmallScreen" />
|
||||||
|
|
||||||
<n-tooltip trigger="hover">
|
<n-tooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button
|
<c-button
|
||||||
round
|
round
|
||||||
type="primary"
|
|
||||||
tag="a"
|
|
||||||
href="https://www.buymeacoffee.com/cthmsst"
|
href="https://www.buymeacoffee.com/cthmsst"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -139,15 +120,15 @@ const tools = computed<ToolCategory[]>(() => [
|
||||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
||||||
>
|
>
|
||||||
Buy me a coffee
|
Buy me a coffee
|
||||||
<n-icon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
|
<NIcon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
|
||||||
</n-button>
|
</c-button>
|
||||||
</template>
|
</template>
|
||||||
❤ Support IT Tools development !
|
❤ Support IT Tools development !
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</template>
|
</template>
|
||||||
</menu-layout>
|
</MenuLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
@ -165,8 +146,8 @@ const tools = computed<ToolCategory[]>(() => [
|
||||||
.support-button {
|
.support-button {
|
||||||
background: rgb(37, 99, 108);
|
background: rgb(37, 99, 108);
|
||||||
background: linear-gradient(48deg, rgba(37, 99, 108, 1) 0%, rgba(59, 149, 111, 1) 60%, rgba(20, 160, 88, 1) 100%);
|
background: linear-gradient(48deg, rgba(37, 99, 108, 1) 0%, rgba(59, 149, 111, 1) 60%, rgba(20, 160, 88, 1) 100%);
|
||||||
color: #fff;
|
color: #fff !important;
|
||||||
transition: all ease 0.2s;
|
transition: padding ease 0.2s !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { useRoute } from 'vue-router';
|
||||||
import { useHead } from '@vueuse/head';
|
import { useHead } from '@vueuse/head';
|
||||||
import type { HeadObject } from '@vueuse/head';
|
import type { HeadObject } from '@vueuse/head';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import BaseLayout from './base.layout.vue';
|
||||||
import FavoriteButton from '@/components/FavoriteButton.vue';
|
import FavoriteButton from '@/components/FavoriteButton.vue';
|
||||||
import type { Tool } from '@/tools/tools.types';
|
import type { Tool } from '@/tools/tools.types';
|
||||||
import BaseLayout from './base.layout.vue';
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
@ -26,18 +26,18 @@ useHead(head);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<base-layout>
|
<BaseLayout>
|
||||||
<div class="tool-layout">
|
<div class="tool-layout">
|
||||||
<div class="tool-header">
|
<div class="tool-header">
|
||||||
<n-space align="center" justify="space-between" :wrap="false">
|
<div flex flex-nowrap items-center justify-between>
|
||||||
<n-h1>
|
<n-h1>
|
||||||
{{ route.meta.name }}
|
{{ route.meta.name }}
|
||||||
</n-h1>
|
</n-h1>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<favorite-button :tool="{name: route.meta.name} as Tool" />
|
<FavoriteButton :tool="{ name: route.meta.name } as Tool" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-space>
|
|
||||||
|
|
||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ useHead(head);
|
||||||
<div class="tool-content">
|
<div class="tool-content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</base-layout>
|
</BaseLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import { createHead } from '@vueuse/head';
|
import { createHead } from '@vueuse/head';
|
||||||
// eslint-disable-next-line import/no-unresolved
|
|
||||||
import { registerSW } from 'virtual:pwa-register';
|
import { registerSW } from 'virtual:pwa-register';
|
||||||
import { plausible } from './plugins/plausible.plugin';
|
import { plausible } from './plugins/plausible.plugin';
|
||||||
|
|
||||||
import 'virtual:uno.css';
|
import 'virtual:uno.css';
|
||||||
|
|
||||||
registerSW();
|
|
||||||
|
|
||||||
import { naive } from './plugins/naive.plugin';
|
import { naive } from './plugins/naive.plugin';
|
||||||
|
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
|
||||||
|
registerSW();
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
|
|
7
src/modules/shared/date.models.ts
Normal file
7
src/modules/shared/date.models.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
export { getUrlFriendlyDateTime };
|
||||||
|
|
||||||
|
function getUrlFriendlyDateTime({ date = new Date() }: { date?: Date } = {}) {
|
||||||
|
return format(date, 'yyyy-MM-dd-HH-mm-ss');
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ function useTracker() {
|
||||||
const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible');
|
const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible');
|
||||||
|
|
||||||
if (_.isNil(plausible)) {
|
if (_.isNil(plausible)) {
|
||||||
throw new Error('Plausible must be instantiated');
|
throw new TypeError('Plausible must be instantiated');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tracker = createTrackerService({ plausible });
|
const tracker = createTrackerService({ plausible });
|
||||||
|
|
|
@ -9,12 +9,18 @@ useHead({ title: 'Page not found - IT Tools' });
|
||||||
<div mt-20 flex flex-col items-center>
|
<div mt-20 flex flex-col items-center>
|
||||||
<n-icon :component="Coffee" size="100" depth="3" />
|
<n-icon :component="Coffee" size="100" depth="3" />
|
||||||
|
|
||||||
<n-h1 m-0 mt-3>404 Not Found</n-h1>
|
<n-h1 m-0 mt-3>
|
||||||
<n-text mt-4 block depth="3">Sorry, this page does not seem to exist</n-text>
|
404 Not Found
|
||||||
<n-text mb-8 block depth="3">Maybe the cache is doing tricky things, try force-refreshing?</n-text>
|
</n-h1>
|
||||||
|
<n-text mt-4 block depth="3">
|
||||||
|
Sorry, this page does not seem to exist
|
||||||
|
</n-text>
|
||||||
|
<n-text mb-8 block depth="3">
|
||||||
|
Maybe the cache is doing tricky things, try force-refreshing?
|
||||||
|
</n-text>
|
||||||
|
|
||||||
<router-link to="/" #="{ navigate, href }" custom>
|
<c-button to="/">
|
||||||
<n-button tag="a" :href="href" secondary @click="navigate"> Back home </n-button>
|
Back home
|
||||||
</router-link>
|
</c-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
|
||||||
import { useHead } from '@vueuse/head';
|
import { useHead } from '@vueuse/head';
|
||||||
|
import { useTracker } from '@/modules/tracker/tracker.services';
|
||||||
|
|
||||||
useHead({ title: 'About - IT Tools' });
|
useHead({ title: 'About - IT Tools' });
|
||||||
const { tracker } = useTracker();
|
const { tracker } = useTracker();
|
||||||
|
@ -11,42 +11,33 @@ const { tracker } = useTracker();
|
||||||
<n-h1>About</n-h1>
|
<n-h1>About</n-h1>
|
||||||
<n-p>
|
<n-p>
|
||||||
This wonderful website, made with ❤ by
|
This wonderful website, made with ❤ by
|
||||||
<n-button text tag="a" href="https://github.com/CorentinTh" target="_blank" rel="noopener" type="primary">
|
<c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener">
|
||||||
Corentin Thomasset </n-button
|
Corentin Thomasset
|
||||||
>, aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to
|
</c-link>,
|
||||||
share it to people you think may find it useful too and don't forget to pin it in your shortcut bar !
|
aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to share
|
||||||
|
it to people you think may find it useful too and don't forget to pin it in your shortcut bar !
|
||||||
</n-p>
|
</n-p>
|
||||||
<n-p>
|
<n-p>
|
||||||
IT Tools is open-source (under the MIT license) and free, and will always be, but it cost me money to host and
|
IT Tools is open-source (under the MIT license) and free, and will always be, but it cost me money to host and
|
||||||
renew the domain name, if you want to support my work, and encourage me to add more tools, please consider
|
renew the domain name, if you want to support my work, and encourage me to add more tools, please consider
|
||||||
supporting by
|
supporting by
|
||||||
<n-button
|
<c-link
|
||||||
type="primary"
|
|
||||||
tag="a"
|
|
||||||
text
|
|
||||||
href="https://www.buymeacoffee.com/cthmsst"
|
href="https://www.buymeacoffee.com/cthmsst"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
||||||
>
|
>
|
||||||
sponsoring me </n-button
|
sponsoring me
|
||||||
>.
|
</c-link>.
|
||||||
</n-p>
|
</n-p>
|
||||||
|
|
||||||
<n-h2>Technologies</n-h2>
|
<n-h2>Technologies</n-h2>
|
||||||
<n-p>
|
<n-p>
|
||||||
IT Tools is made in Vue JS (vue 3) with the the naive-ui component library and is hosted and continuously deployed
|
IT Tools is made in Vue JS (vue 3) with the the naive-ui component library and is hosted and continuously deployed
|
||||||
by Vercel. Third party open-source libraries are used in some tools, you may find the complete list in the
|
by Vercel. Third party open-source libraries are used in some tools, you may find the complete list in the
|
||||||
<n-button
|
<c-link href="https://github.com/CorentinTh/it-tools/blob/main/package.json" rel="noopener" target="_blank">
|
||||||
type="primary"
|
|
||||||
tag="a"
|
|
||||||
text
|
|
||||||
href="https://github.com/CorentinTh/it-tools/blob/main/package.json"
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
package.json
|
package.json
|
||||||
</n-button>
|
</c-link>
|
||||||
file of the repository.
|
file of the repository.
|
||||||
</n-p>
|
</n-p>
|
||||||
|
|
||||||
|
@ -54,30 +45,24 @@ const { tracker } = useTracker();
|
||||||
<n-p>
|
<n-p>
|
||||||
If you need a tool that is currently not present here, and you think can be relevant, you are welcome to submit a
|
If you need a tool that is currently not present here, and you think can be relevant, you are welcome to submit a
|
||||||
feature request in the
|
feature request in the
|
||||||
<n-button
|
<c-link
|
||||||
type="primary"
|
|
||||||
tag="a"
|
|
||||||
text
|
|
||||||
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=enhancement&template=feature_request.md&title=%5BFEAT%5D%20My%20feature"
|
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=enhancement&template=feature_request.md&title=%5BFEAT%5D%20My%20feature"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
issues section
|
issues section
|
||||||
</n-button>
|
</c-link>
|
||||||
in the GitHub repository.
|
in the GitHub repository.
|
||||||
</n-p>
|
</n-p>
|
||||||
<n-p>
|
<n-p>
|
||||||
And if you found a bug, or something broken that doesn't work as expected, please fill a bug report in the
|
And if you found a bug, or something broken that doesn't work as expected, please fill a bug report in the
|
||||||
<n-button
|
<c-link
|
||||||
type="primary"
|
|
||||||
tag="a"
|
|
||||||
text
|
|
||||||
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=bug&template=bug_report.md&title=%5BBUG%5D%20My%20bug"
|
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=bug&template=bug_report.md&title=%5BBUG%5D%20My%20bug"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
issues section
|
issues section
|
||||||
</n-button>
|
</c-link>
|
||||||
in the GitHub repository.
|
in the GitHub repository.
|
||||||
</n-p>
|
</n-p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { config } from '@/config';
|
|
||||||
import { useToolStore } from '@/tools/tools.store';
|
|
||||||
import { Heart } from '@vicons/tabler';
|
import { Heart } from '@vicons/tabler';
|
||||||
import { useHead } from '@vueuse/head';
|
import { useHead } from '@vueuse/head';
|
||||||
import ColoredCard from '../components/ColoredCard.vue';
|
import ColoredCard from '../components/ColoredCard.vue';
|
||||||
import ToolCard from '../components/ToolCard.vue';
|
import ToolCard from '../components/ToolCard.vue';
|
||||||
|
import { useToolStore } from '@/tools/tools.store';
|
||||||
|
import { config } from '@/config';
|
||||||
|
|
||||||
const toolStore = useToolStore();
|
const toolStore = useToolStore();
|
||||||
|
|
||||||
|
@ -16,25 +16,23 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
|
||||||
<div class="grid-wrapper">
|
<div class="grid-wrapper">
|
||||||
<n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
<n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<colored-card title="You like it-tools?" :icon="Heart">
|
<ColoredCard title="You like it-tools?" :icon="Heart">
|
||||||
Give us a star on
|
Give us a star on
|
||||||
<a
|
<a
|
||||||
href="https://github.com/CorentinTh/it-tools"
|
href="https://github.com/CorentinTh/it-tools"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
aria-label="IT-Tools' GitHub repository"
|
aria-label="IT-Tools' GitHub repository"
|
||||||
>GitHub</a
|
>GitHub</a>
|
||||||
>
|
|
||||||
or follow us on
|
or follow us on
|
||||||
<a
|
<a
|
||||||
href="https://twitter.com/ittoolsdottech"
|
href="https://twitter.com/ittoolsdottech"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
aria-label="IT-Tools' Twitter account"
|
aria-label="IT-Tools' Twitter account"
|
||||||
>Twitter</a
|
>Twitter</a>! Thank you
|
||||||
>! Thank you
|
|
||||||
<n-icon :component="Heart" />
|
<n-icon :component="Heart" />
|
||||||
</colored-card>
|
</ColoredCard>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
|
||||||
|
@ -43,7 +41,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
|
||||||
<n-h3>Your favorite tools</n-h3>
|
<n-h3>Your favorite tools</n-h3>
|
||||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||||
<n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name">
|
<n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name">
|
||||||
<tool-card :tool="tool" />
|
<ToolCard :tool="tool" />
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,7 +51,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
|
||||||
<n-h3>Newest tools</n-h3>
|
<n-h3>Newest tools</n-h3>
|
||||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||||
<n-gi v-for="tool in toolStore.newTools" :key="tool.name">
|
<n-gi v-for="tool in toolStore.newTools" :key="tool.name">
|
||||||
<tool-card :tool="tool" />
|
<ToolCard :tool="tool" />
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,7 +60,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
|
||||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||||
<n-gi v-for="tool in toolStore.tools" :key="tool.name">
|
<n-gi v-for="tool in toolStore.tools" :key="tool.name">
|
||||||
<transition>
|
<transition>
|
||||||
<tool-card :tool="tool" />
|
<ToolCard :tool="tool" />
|
||||||
</transition>
|
</transition>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { config } from '@/config';
|
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
|
|
||||||
import Plausible from 'plausible-tracker';
|
import Plausible from 'plausible-tracker';
|
||||||
import type { App } from 'vue';
|
import type { App } from 'vue';
|
||||||
|
import { config } from '@/config';
|
||||||
|
|
||||||
function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> {
|
function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> {
|
||||||
return {
|
return {
|
||||||
|
@ -15,11 +15,11 @@ function createPlausibleInstance({
|
||||||
config,
|
config,
|
||||||
}: {
|
}: {
|
||||||
config: {
|
config: {
|
||||||
isTrackerEnabled: boolean;
|
isTrackerEnabled: boolean
|
||||||
domain: string;
|
domain: string
|
||||||
apiHost: string;
|
apiHost: string
|
||||||
trackLocalhost: boolean;
|
trackLocalhost: boolean
|
||||||
};
|
}
|
||||||
}) {
|
}) {
|
||||||
if (config.isTrackerEnabled) {
|
if (config.isTrackerEnabled) {
|
||||||
return Plausible(config);
|
return Plausible(config);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import HomePage from './pages/Home.page.vue';
|
||||||
import NotFound from './pages/404.page.vue';
|
import NotFound from './pages/404.page.vue';
|
||||||
import { tools } from './tools';
|
import { tools } from './tools';
|
||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
|
import { routes as demoRoutes } from './ui/demo/demo.routes';
|
||||||
|
|
||||||
const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
|
const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
|
||||||
path,
|
path,
|
||||||
|
@ -14,7 +15,7 @@ const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
|
||||||
const toolsRedirectRoutes = tools
|
const toolsRedirectRoutes = tools
|
||||||
.filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0)
|
.filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0)
|
||||||
.flatMap(
|
.flatMap(
|
||||||
({ path, redirectFrom }) => redirectFrom?.map((redirectSource) => ({ path: redirectSource, redirect: path })) ?? [],
|
({ path, redirectFrom }) => redirectFrom?.map(redirectSource => ({ path: redirectSource, redirect: path })) ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
@ -32,6 +33,7 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
...toolsRoutes,
|
...toolsRoutes,
|
||||||
...toolsRedirectRoutes,
|
...toolsRedirectRoutes,
|
||||||
|
...(config.app.env === 'development' ? demoRoutes : []),
|
||||||
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
|
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
6
src/shims.d.ts
vendored
6
src/shims.d.ts
vendored
|
@ -8,3 +8,9 @@ declare module '*.md' {
|
||||||
const Component: ComponentOptions;
|
const Component: ComponentOptions;
|
||||||
export default Component;
|
export default Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '~icons/*' {
|
||||||
|
import { FunctionalComponent, SVGAttributes } from 'vue';
|
||||||
|
const component: FunctionalComponent<SVGAttributes>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMediaQuery, useStorage } from '@vueuse/core';
|
import { useMediaQuery, useStorage } from '@vueuse/core';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { watch, type Ref } from 'vue';
|
import { type Ref, watch } from 'vue';
|
||||||
|
|
||||||
export const useStyleStore = defineStore('style', {
|
export const useStyleStore = defineStore('style', {
|
||||||
state: () => {
|
state: () => {
|
||||||
|
@ -8,7 +8,7 @@ export const useStyleStore = defineStore('style', {
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 700px)');
|
const isSmallScreen = useMediaQuery('(max-width: 700px)');
|
||||||
const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>;
|
const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>;
|
||||||
|
|
||||||
watch(isSmallScreen, (v) => (isMenuCollapsed.value = v));
|
watch(isSmallScreen, v => (isMenuCollapsed.value = v));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDarkTheme,
|
isDarkTheme,
|
||||||
|
|
|
@ -1,45 +1,12 @@
|
||||||
<template>
|
|
||||||
<n-card title="Base64 to file">
|
|
||||||
<n-form-item
|
|
||||||
:feedback="base64InputValidation.message"
|
|
||||||
:validation-status="base64InputValidation.status"
|
|
||||||
:show-label="false"
|
|
||||||
>
|
|
||||||
<n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button :disabled="base64Input === '' || !base64InputValidation.isValid" secondary @click="downloadFile()">
|
|
||||||
Download file
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-card title="File to base64">
|
|
||||||
<n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image">
|
|
||||||
<n-upload-dragger>
|
|
||||||
<div mb-2>
|
|
||||||
<n-icon size="35" :depth="3" :component="Upload" />
|
|
||||||
</div>
|
|
||||||
<n-text style="font-size: 14px"> Click or drag a file to this area to upload </n-text>
|
|
||||||
</n-upload-dragger>
|
|
||||||
</n-upload>
|
|
||||||
|
|
||||||
<n-input :value="fileBase64" type="textarea" readonly placeholder="File in base64 will be here" />
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button secondary @click="copyFileBase64()"> Copy </n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Upload } from '@vicons/tabler';
|
||||||
|
import { useBase64 } from '@vueuse/core';
|
||||||
|
import type { UploadFileInfo } from 'naive-ui';
|
||||||
|
import { type Ref, ref } from 'vue';
|
||||||
import { useCopy } from '@/composable/copy';
|
import { useCopy } from '@/composable/copy';
|
||||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||||
import { useValidation } from '@/composable/validation';
|
import { useValidation } from '@/composable/validation';
|
||||||
import { isValidBase64 } from '@/utils/base64';
|
import { isValidBase64 } from '@/utils/base64';
|
||||||
import { Upload } from '@vicons/tabler';
|
|
||||||
import { useBase64 } from '@vueuse/core';
|
|
||||||
import type { UploadFileInfo } from 'naive-ui';
|
|
||||||
import { ref, type Ref } from 'vue';
|
|
||||||
|
|
||||||
const base64Input = ref('');
|
const base64Input = ref('');
|
||||||
const { download } = useDownloadFileFromBase64({ source: base64Input });
|
const { download } = useDownloadFileFromBase64({ source: base64Input });
|
||||||
|
@ -48,17 +15,20 @@ const base64InputValidation = useValidation({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
message: 'Invalid base 64 string',
|
message: 'Invalid base 64 string',
|
||||||
validator: (value) => isValidBase64(value.trim()),
|
validator: value => isValidBase64(value.trim()),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
function downloadFile() {
|
function downloadFile() {
|
||||||
if (!base64InputValidation.isValid) return;
|
if (!base64InputValidation.isValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
download();
|
download();
|
||||||
} catch (_) {
|
}
|
||||||
|
catch (_) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,12 +46,47 @@ async function onUpload({ file: { file } }: { file: UploadFileInfo }) {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<template>
|
||||||
.n-input,
|
<c-card title="Base64 to file">
|
||||||
.n-upload {
|
<c-input-text
|
||||||
margin-bottom: 15px;
|
v-model:value="base64Input"
|
||||||
}
|
multiline
|
||||||
|
placeholder="Put your base64 file string here..."
|
||||||
|
rows="5"
|
||||||
|
:validation="base64InputValidation"
|
||||||
|
mb-2
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()">
|
||||||
|
Download file
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card title="File to base64">
|
||||||
|
<n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image">
|
||||||
|
<n-upload-dragger>
|
||||||
|
<div mb-2>
|
||||||
|
<n-icon size="35" :depth="3" :component="Upload" />
|
||||||
|
</div>
|
||||||
|
<n-text style="font-size: 14px">
|
||||||
|
Click or drag a file to this area to upload
|
||||||
|
</n-text>
|
||||||
|
</n-upload-dragger>
|
||||||
|
</n-upload>
|
||||||
|
|
||||||
|
<c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 />
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<c-button @click="copyFileBase64()">
|
||||||
|
Copy
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
::v-deep(.n-upload-trigger) {
|
::v-deep(.n-upload-trigger) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { defineTool } from '../tool';
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'Base64 file converter',
|
name: 'Base64 file converter',
|
||||||
path: '/base64-file-converter',
|
path: '/base64-file-converter',
|
||||||
description: "Convert string, files or images into a it's base64 representation.",
|
description: 'Convert string, files or images into a it\'s base64 representation.',
|
||||||
keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'],
|
keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'],
|
||||||
component: () => import('./base64-file-converter.vue'),
|
component: () => import('./base64-file-converter.vue'),
|
||||||
icon: FileDigit,
|
icon: FileDigit,
|
||||||
|
|
|
@ -1,55 +1,91 @@
|
||||||
<template>
|
|
||||||
<n-card title="String to base64">
|
|
||||||
<n-form-item label="String to encode">
|
|
||||||
<n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." rows="5" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="Base64 of string">
|
|
||||||
<n-input
|
|
||||||
:value="base64Output"
|
|
||||||
type="textarea"
|
|
||||||
readonly
|
|
||||||
placeholder="The base64 encoding of your string will be here"
|
|
||||||
rows="5"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button secondary @click="copyTextBase64()"> Copy base64 </n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-card title="Base64 to string">
|
|
||||||
<n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs">
|
|
||||||
<n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="Decoded string">
|
|
||||||
<n-input :value="textOutput" type="textarea" readonly placeholder="The decoded string will be here" rows="5" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button secondary @click="copyText()"> Copy decoded string </n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
import { useCopy } from '@/composable/copy';
|
import { useCopy } from '@/composable/copy';
|
||||||
import { useValidation } from '@/composable/validation';
|
|
||||||
import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64';
|
import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64';
|
||||||
import { withDefaultOnError } from '@/utils/defaults';
|
import { withDefaultOnError } from '@/utils/defaults';
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
const encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false);
|
||||||
|
const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false);
|
||||||
|
|
||||||
const textInput = ref('');
|
const textInput = ref('');
|
||||||
const base64Output = computed(() => textToBase64(textInput.value));
|
const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.value }));
|
||||||
const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' });
|
const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' });
|
||||||
|
|
||||||
const base64Input = ref('');
|
const base64Input = ref('');
|
||||||
const textOutput = computed(() => withDefaultOnError(() => base64ToText(base64Input.value.trim()), ''));
|
const textOutput = computed(() =>
|
||||||
|
withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''),
|
||||||
|
);
|
||||||
const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' });
|
const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' });
|
||||||
const b64Validation = useValidation({
|
const b64ValidationRules = [
|
||||||
source: base64Input,
|
{
|
||||||
rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }],
|
message: 'Invalid base64 string',
|
||||||
});
|
validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const b64ValidationWatch = [decodeUrlSafe];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-card title="String to base64">
|
||||||
|
<n-form-item label="Encode URL safe" label-placement="left">
|
||||||
|
<n-switch v-model:value="encodeUrlSafe" />
|
||||||
|
</n-form-item>
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="textInput"
|
||||||
|
multiline
|
||||||
|
placeholder="Put your string here..."
|
||||||
|
rows="5"
|
||||||
|
label="String to encode"
|
||||||
|
raw-text
|
||||||
|
mb-5
|
||||||
|
/>
|
||||||
|
|
||||||
|
<c-input-text
|
||||||
|
label="Base64 of string"
|
||||||
|
:value="base64Output"
|
||||||
|
multiline
|
||||||
|
readonly
|
||||||
|
placeholder="The base64 encoding of your string will be here"
|
||||||
|
rows="5"
|
||||||
|
mb-5
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<c-button @click="copyTextBase64()">
|
||||||
|
Copy base64
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card title="Base64 to string">
|
||||||
|
<n-form-item label="Decode URL safe" label-placement="left">
|
||||||
|
<n-switch v-model:value="decodeUrlSafe" />
|
||||||
|
</n-form-item>
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="base64Input"
|
||||||
|
multiline
|
||||||
|
placeholder="Your base64 string..."
|
||||||
|
rows="5"
|
||||||
|
:validation-rules="b64ValidationRules"
|
||||||
|
:validation-watch="b64ValidationWatch"
|
||||||
|
label="Base64 string to decode"
|
||||||
|
mb-5
|
||||||
|
/>
|
||||||
|
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="textOutput"
|
||||||
|
label="Decoded string"
|
||||||
|
placeholder="The decoded string will be here"
|
||||||
|
multiline
|
||||||
|
rows="5"
|
||||||
|
readonly
|
||||||
|
mb-5
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<c-button @click="copyText()">
|
||||||
|
Copy decoded string
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,37 +1,7 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-form-item label="Username">
|
|
||||||
<n-input v-model:value="username" placeholder="Your username..." clearable />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Password">
|
|
||||||
<n-input
|
|
||||||
v-model:value="password"
|
|
||||||
placeholder="Your password..."
|
|
||||||
type="password"
|
|
||||||
show-password-on="click"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<n-card>
|
|
||||||
<n-statistic label="Authorization header:" class="header">
|
|
||||||
<n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none">
|
|
||||||
{{ header }}
|
|
||||||
</n-scrollbar>
|
|
||||||
</n-statistic>
|
|
||||||
</n-card>
|
|
||||||
<br />
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button secondary @click="copy">Copy header</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
import { useCopy } from '@/composable/copy';
|
import { useCopy } from '@/composable/copy';
|
||||||
import { textToBase64 } from '@/utils/base64';
|
import { textToBase64 } from '@/utils/base64';
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
|
||||||
const username = ref('');
|
const username = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
|
@ -40,6 +10,34 @@ const header = computed(() => `Authorization: Basic ${textToBase64(`${username.v
|
||||||
const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' });
|
const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 />
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="Your password..."
|
||||||
|
clearable
|
||||||
|
raw-text
|
||||||
|
mb-2
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<c-card>
|
||||||
|
<n-statistic label="Authorization header:" class="header">
|
||||||
|
<n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none">
|
||||||
|
{{ header }}
|
||||||
|
</n-scrollbar>
|
||||||
|
</n-statistic>
|
||||||
|
</c-card>
|
||||||
|
<div mt-5 flex justify-center>
|
||||||
|
<c-button @click="copy">
|
||||||
|
Copy header
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
::v-deep(.n-statistic-value__content) {
|
::v-deep(.n-statistic-value__content) {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
|
|
@ -1,63 +1,8 @@
|
||||||
<template>
|
|
||||||
<n-card title="Hash">
|
|
||||||
<n-form label-width="120">
|
|
||||||
<n-form-item label="Your string: " label-placement="left">
|
|
||||||
<n-input
|
|
||||||
v-model:value="input"
|
|
||||||
placeholder="Your string to bcrypt..."
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Salt count: " label-placement="left">
|
|
||||||
<n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
|
|
||||||
</n-form-item>
|
|
||||||
<n-input :value="hashed" readonly style="text-align: center" />
|
|
||||||
</n-form>
|
|
||||||
<br />
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button secondary @click="copy"> Copy hash </n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-card title="Compare string with hash">
|
|
||||||
<n-form label-width="120">
|
|
||||||
<n-form-item label="Your string: " label-placement="left">
|
|
||||||
<n-input
|
|
||||||
v-model:value="compareString"
|
|
||||||
placeholder="Your string to compare..."
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Your hash: " label-placement="left">
|
|
||||||
<n-input
|
|
||||||
v-model:value="compareHash"
|
|
||||||
placeholder="Your hahs to compare..."
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
|
|
||||||
<div class="compare-result" :class="{ positive: compareMatch }">
|
|
||||||
{{ compareMatch ? 'Yes' : 'No' }}
|
|
||||||
</div>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { hashSync, compareSync } from 'bcryptjs';
|
import { compareSync, hashSync } from 'bcryptjs';
|
||||||
import { useCopy } from '@/composable/copy';
|
|
||||||
import { useThemeVars } from 'naive-ui';
|
import { useThemeVars } from 'naive-ui';
|
||||||
|
import { useCopy } from '@/composable/copy';
|
||||||
|
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
|
|
||||||
|
@ -71,6 +16,47 @@ const compareHash = ref('');
|
||||||
const compareMatch = computed(() => compareSync(compareString.value, compareHash.value));
|
const compareMatch = computed(() => compareSync(compareString.value, compareHash.value));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-card title="Hash">
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="input"
|
||||||
|
placeholder="Your string to bcrypt..."
|
||||||
|
raw-text
|
||||||
|
label="Your string: "
|
||||||
|
label-position="left"
|
||||||
|
label-width="120px"
|
||||||
|
mb-2
|
||||||
|
/>
|
||||||
|
<n-form-item label="Salt count: " label-placement="left" label-width="120">
|
||||||
|
<n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<c-input-text :value="hashed" readonly text-center />
|
||||||
|
|
||||||
|
<div mt-5 flex justify-center>
|
||||||
|
<c-button @click="copy">
|
||||||
|
Copy hash
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card title="Compare string with hash">
|
||||||
|
<n-form label-width="120">
|
||||||
|
<n-form-item label="Your string: " label-placement="left">
|
||||||
|
<c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Your hash: " label-placement="left">
|
||||||
|
<c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
|
||||||
|
<div class="compare-result" :class="{ positive: compareMatch }">
|
||||||
|
{{ compareMatch ? 'Yes' : 'No' }}
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.compare-result {
|
.compare-result {
|
||||||
color: v-bind('themeVars.errorColor');
|
color: v-bind('themeVars.errorColor');
|
||||||
|
|
|
@ -13,7 +13,7 @@ function computeAverage({ data }: { data: number[] }) {
|
||||||
function computeVariance({ data }: { data: number[] }) {
|
function computeVariance({ data }: { data: number[] }) {
|
||||||
const mean = computeAverage({ data });
|
const mean = computeAverage({ data });
|
||||||
|
|
||||||
const squaredDiffs = data.map((value) => Math.pow(value - mean, 2));
|
const squaredDiffs = data.map(value => (value - mean) ** 2);
|
||||||
|
|
||||||
return computeAverage({ data: squaredDiffs });
|
return computeAverage({ data: squaredDiffs });
|
||||||
}
|
}
|
||||||
|
@ -24,11 +24,11 @@ function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; heade
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = Object.keys(data[0]);
|
const headers = Object.keys(data[0]);
|
||||||
const rows = data.map((obj) => Object.values(obj));
|
const rows = data.map(obj => Object.values(obj));
|
||||||
|
|
||||||
const headerRow = `| ${headers.map((header) => headerMap[header] ?? header).join(' | ')} |`;
|
const headerRow = `| ${headers.map(header => headerMap[header] ?? header).join(' | ')} |`;
|
||||||
const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`;
|
const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`;
|
||||||
const dataRows = rows.map((row) => `| ${row.join(' | ')} |`).join('\n');
|
const dataRows = rows.map(row => `| ${row.join(' | ')} |`).join('\n');
|
||||||
|
|
||||||
return `${headerRow}\n${separatorRow}\n${dataRows}`;
|
return `${headerRow}\n${separatorRow}\n${dataRows}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,91 +1,9 @@
|
||||||
<template>
|
|
||||||
<n-scrollbar style="flex: 1" x-scrollable>
|
|
||||||
<n-space :wrap="false" style="flex: 1" justify="center" :size="0">
|
|
||||||
<div v-for="(suite, index) of suites" :key="index">
|
|
||||||
<n-card style="width: 292px; margin: 0 8px 5px">
|
|
||||||
<n-form-item label="Suite name:" :show-feedback="false" label-placement="left">
|
|
||||||
<n-input v-model:value="suite.title" placeholder="Suite name..." />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-divider></n-divider>
|
|
||||||
<n-form-item label="Suite values" :show-feedback="false">
|
|
||||||
<dynamic-values v-model:values="suite.data" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button v-if="suites.length > 1" quaternary @click="suites.splice(index, 1)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Trash" depth="3" />
|
|
||||||
</template>
|
|
||||||
Delete suite
|
|
||||||
</n-button>
|
|
||||||
<n-button quaternary @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Plus" depth="3" />
|
|
||||||
</template>
|
|
||||||
Add suite
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</n-space>
|
|
||||||
<br />
|
|
||||||
</n-scrollbar>
|
|
||||||
|
|
||||||
<div style="flex: 0 0 100%">
|
|
||||||
<div style="max-width: 600px; margin: 0 auto">
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-form-item label="Unit:" label-placement="left">
|
|
||||||
<n-input v-model:value="unit" placeholder="Unit (eg: ms)" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-button
|
|
||||||
tertiary
|
|
||||||
@click="
|
|
||||||
suites = [
|
|
||||||
{ title: 'Suite 1', data: [] },
|
|
||||||
{ title: 'Suite 2', data: [] },
|
|
||||||
]
|
|
||||||
"
|
|
||||||
>Reset suites</n-button
|
|
||||||
>
|
|
||||||
</n-space>
|
|
||||||
|
|
||||||
<n-table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ header.position }}</th>
|
|
||||||
<th>{{ header.title }}</th>
|
|
||||||
<th>{{ header.size }}</th>
|
|
||||||
<th>{{ header.mean }}</th>
|
|
||||||
<th>{{ header.variance }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="{ title, size, mean, variance, position } of results" :key="title">
|
|
||||||
<td>{{ position }}</td>
|
|
||||||
<td>{{ title }}</td>
|
|
||||||
<td>{{ size }}</td>
|
|
||||||
<td>{{ mean }}</td>
|
|
||||||
<td>{{ variance }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</n-table>
|
|
||||||
<br />
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button tertiary @click="copyAsMarkdown">Copy as markdown table</n-button>
|
|
||||||
<n-button tertiary @click="copyAsBulletList">Copy as bullet list</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Trash, Plus } from '@vicons/tabler';
|
import { Plus, Trash } from '@vicons/tabler';
|
||||||
import { useClipboard, useStorage } from '@vueuse/core';
|
import { useClipboard, useStorage } from '@vueuse/core';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { computeAverage, computeVariance, arrayToMarkdownTable } from './benchmark-builder.models';
|
import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models';
|
||||||
import DynamicValues from './dynamic-values.vue';
|
import DynamicValues from './dynamic-values.vue';
|
||||||
|
|
||||||
const suites = useStorage('benchmark-builder:suites', [
|
const suites = useStorage('benchmark-builder:suites', [
|
||||||
|
@ -116,8 +34,8 @@ const results = computed(() => {
|
||||||
const deltaWithBestMean = mean - bestMean;
|
const deltaWithBestMean = mean - bestMean;
|
||||||
const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean);
|
const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean);
|
||||||
|
|
||||||
const comparisonValues: string =
|
const comparisonValues: string
|
||||||
index !== 0 && bestMean !== mean ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
|
= (index !== 0 && bestMean !== mean) ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
position: index + 1,
|
position: index + 1,
|
||||||
|
@ -159,4 +77,87 @@ function copyAsBulletList() {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<template>
|
||||||
|
<n-scrollbar style="flex: 1" x-scrollable>
|
||||||
|
<div mb-5 flex flex-1 flex-nowrap justify-center gap-12px>
|
||||||
|
<div v-for="(suite, index) of suites" :key="index">
|
||||||
|
<c-card style="width: 294px">
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="suite.title"
|
||||||
|
label-position="left"
|
||||||
|
label="Suite name"
|
||||||
|
placeholder="Suite name..."
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
<n-form-item label="Suite values" :show-feedback="false">
|
||||||
|
<DynamicValues v-model:values="suite.data" />
|
||||||
|
</n-form-item>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)">
|
||||||
|
<n-icon :component="Trash" depth="3" mr-2 size="18" />
|
||||||
|
Delete suite
|
||||||
|
</c-button>
|
||||||
|
<c-button
|
||||||
|
variant="text"
|
||||||
|
@click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })"
|
||||||
|
>
|
||||||
|
<n-icon :component="Plus" depth="3" mr-2 size="18" />
|
||||||
|
Add suite
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-scrollbar>
|
||||||
|
|
||||||
|
<div style="flex: 0 0 100%">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto">
|
||||||
|
<div mx-auto max-w-sm flex justify-center gap-3>
|
||||||
|
<c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 />
|
||||||
|
|
||||||
|
<c-button
|
||||||
|
@click="
|
||||||
|
suites = [
|
||||||
|
{ title: 'Suite 1', data: [] },
|
||||||
|
{ title: 'Suite 2', data: [] },
|
||||||
|
]
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Reset suites
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ header.position }}</th>
|
||||||
|
<th>{{ header.title }}</th>
|
||||||
|
<th>{{ header.size }}</th>
|
||||||
|
<th>{{ header.mean }}</th>
|
||||||
|
<th>{{ header.variance }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="{ title, size, mean, variance, position } of results" :key="title">
|
||||||
|
<td>{{ position }}</td>
|
||||||
|
<td>{{ title }}</td>
|
||||||
|
<td>{{ size }}</td>
|
||||||
|
<td>{{ mean }}</td>
|
||||||
|
<td>{{ variance }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</n-table>
|
||||||
|
<div mt-5 flex justify-center gap-3>
|
||||||
|
<c-button @click="copyAsMarkdown">
|
||||||
|
Copy as markdown table
|
||||||
|
</c-button>
|
||||||
|
<c-button @click="copyAsBulletList">
|
||||||
|
Copy as bullet list
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,45 +1,15 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-space v-for="(value, index) of values" :key="index" :wrap="false" style="margin-bottom: 5px" :size="5">
|
|
||||||
<n-input-number
|
|
||||||
:ref="refs.set"
|
|
||||||
v-model:value="values[index]"
|
|
||||||
:show-button="false"
|
|
||||||
placeholder="Set your measure..."
|
|
||||||
autofocus
|
|
||||||
@keydown.enter="onInputEnter(index)"
|
|
||||||
/>
|
|
||||||
<n-tooltip>
|
|
||||||
<template #trigger>
|
|
||||||
<n-button circle quaternary @click="values.splice(index, 1)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Trash" depth="3" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
Delete value
|
|
||||||
</n-tooltip>
|
|
||||||
</n-space>
|
|
||||||
|
|
||||||
<n-button tertiary @click="addValue">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Plus" />
|
|
||||||
</template>
|
|
||||||
Add a measure
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Trash, Plus } from '@vicons/tabler';
|
import { Plus, Trash } from '@vicons/tabler';
|
||||||
import { useTemplateRefsList, useVModel } from '@vueuse/core';
|
import { useTemplateRefsList, useVModel } from '@vueuse/core';
|
||||||
import { NInputNumber } from 'naive-ui';
|
import { NInputNumber } from 'naive-ui';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ values: (number | null)[] }>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:values']);
|
||||||
|
|
||||||
const refs = useTemplateRefsList<typeof NInputNumber>();
|
const refs = useTemplateRefsList<typeof NInputNumber>();
|
||||||
|
|
||||||
const props = defineProps<{ values: (number | null)[] }>();
|
|
||||||
const emit = defineEmits(['update:values']);
|
|
||||||
const values = useVModel(props, 'values', emit);
|
const values = useVModel(props, 'values', emit);
|
||||||
|
|
||||||
async function addValue() {
|
async function addValue() {
|
||||||
|
@ -58,4 +28,30 @@ function onInputEnter(index: number) {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2>
|
||||||
|
<NInputNumber
|
||||||
|
:ref="refs.set"
|
||||||
|
v-model:value="values[index]"
|
||||||
|
:show-button="false"
|
||||||
|
placeholder="Set your measure..."
|
||||||
|
autofocus
|
||||||
|
@keydown.enter="onInputEnter(index)"
|
||||||
|
/>
|
||||||
|
<n-tooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<c-button circle variant="text" @click="values.splice(index, 1)">
|
||||||
|
<n-icon :component="Trash" depth="3" size="18" />
|
||||||
|
</c-button>
|
||||||
|
</template>
|
||||||
|
Delete value
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<c-button @click="addValue">
|
||||||
|
<n-icon :component="Plus" depth="3" mr-2 size="18" />
|
||||||
|
Add a measure
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,67 +1,4 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-card>
|
|
||||||
<n-grid cols="3" x-gap="12">
|
|
||||||
<n-gi span="1">
|
|
||||||
<n-form-item label="Language:">
|
|
||||||
<n-select
|
|
||||||
v-model:value="language"
|
|
||||||
:options="Object.keys(languages).map((label) => ({ label, value: label }))"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi span="2">
|
|
||||||
<n-form-item
|
|
||||||
label="Entropy (seed):"
|
|
||||||
:feedback="entropyValidation.message"
|
|
||||||
:validation-status="entropyValidation.status"
|
|
||||||
>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input v-model:value="entropy" placeholder="Your string..." />
|
|
||||||
<n-button @click="refreshEntropy">
|
|
||||||
<n-icon size="22">
|
|
||||||
<Refresh />
|
|
||||||
</n-icon>
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="copyEntropy">
|
|
||||||
<n-icon size="22">
|
|
||||||
<Copy />
|
|
||||||
</n-icon>
|
|
||||||
</n-button>
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item>
|
|
||||||
</n-gi>
|
|
||||||
</n-grid>
|
|
||||||
<n-form-item
|
|
||||||
label="Passphrase (mnemonic):"
|
|
||||||
:feedback="mnemonicValidation.message"
|
|
||||||
:validation-status="mnemonicValidation.status"
|
|
||||||
>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input
|
|
||||||
v-model:value="passphrase"
|
|
||||||
style="text-align: center; flex: 1"
|
|
||||||
placeholder="Your mnemonic..."
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<n-button @click="copyPassphrase">
|
|
||||||
<n-icon size="22" :component="Copy" />
|
|
||||||
</n-button>
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCopy } from '@/composable/copy';
|
|
||||||
import { useValidation } from '@/composable/validation';
|
|
||||||
import { isNotThrowing } from '@/utils/boolean';
|
|
||||||
import { withDefaultOnError } from '@/utils/defaults';
|
|
||||||
import {
|
import {
|
||||||
chineseSimplifiedWordList,
|
chineseSimplifiedWordList,
|
||||||
chineseTraditionalWordList,
|
chineseTraditionalWordList,
|
||||||
|
@ -79,18 +16,22 @@ import {
|
||||||
} from '@it-tools/bip39';
|
} from '@it-tools/bip39';
|
||||||
import { Copy, Refresh } from '@vicons/tabler';
|
import { Copy, Refresh } from '@vicons/tabler';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useCopy } from '@/composable/copy';
|
||||||
|
import { useValidation } from '@/composable/validation';
|
||||||
|
import { isNotThrowing } from '@/utils/boolean';
|
||||||
|
import { withDefaultOnError } from '@/utils/defaults';
|
||||||
|
|
||||||
const languages = {
|
const languages = {
|
||||||
English: englishWordList,
|
'English': englishWordList,
|
||||||
'Chinese simplified': chineseSimplifiedWordList,
|
'Chinese simplified': chineseSimplifiedWordList,
|
||||||
'Chinese traditional': chineseTraditionalWordList,
|
'Chinese traditional': chineseTraditionalWordList,
|
||||||
Czech: czechWordList,
|
'Czech': czechWordList,
|
||||||
French: frenchWordList,
|
'French': frenchWordList,
|
||||||
Italian: italianWordList,
|
'Italian': italianWordList,
|
||||||
Japanese: japaneseWordList,
|
'Japanese': japaneseWordList,
|
||||||
Korean: koreanWordList,
|
'Korean': koreanWordList,
|
||||||
Portuguese: portugueseWordList,
|
'Portuguese': portugueseWordList,
|
||||||
Spanish: spanishWordList,
|
'Spanish': spanishWordList,
|
||||||
};
|
};
|
||||||
|
|
||||||
const entropy = ref(generateEntropy());
|
const entropy = ref(generateEntropy());
|
||||||
|
@ -111,11 +52,11 @@ const entropyValidation = useValidation({
|
||||||
source: entropy,
|
source: entropy,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
validator: (value) => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
|
validator: value => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
|
||||||
message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4',
|
message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
validator: (value) => /^[a-fA-F0-9]*$/.test(value),
|
validator: value => /^[a-fA-F0-9]*$/.test(value),
|
||||||
message: 'Entropy should be an hexadecimal string',
|
message: 'Entropy should be an hexadecimal string',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -125,7 +66,7 @@ const mnemonicValidation = useValidation({
|
||||||
source: passphrase,
|
source: passphrase,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
validator: (value) => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
|
validator: value => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
|
||||||
message: 'Invalid mnemonic',
|
message: 'Invalid mnemonic',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -138,3 +79,53 @@ function refreshEntropy() {
|
||||||
const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' });
|
const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' });
|
||||||
const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' });
|
const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-grid cols="3" x-gap="12">
|
||||||
|
<n-gi span="1">
|
||||||
|
<n-form-item label="Language:">
|
||||||
|
<n-select
|
||||||
|
v-model:value="language"
|
||||||
|
:options="Object.keys(languages).map((label) => ({ label, value: label }))"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi span="2">
|
||||||
|
<n-form-item
|
||||||
|
label="Entropy (seed):"
|
||||||
|
:feedback="entropyValidation.message"
|
||||||
|
:validation-status="entropyValidation.status"
|
||||||
|
>
|
||||||
|
<n-input-group>
|
||||||
|
<c-input-text v-model:value="entropy" placeholder="Your string..." />
|
||||||
|
|
||||||
|
<c-button @click="refreshEntropy">
|
||||||
|
<n-icon size="22">
|
||||||
|
<Refresh />
|
||||||
|
</n-icon>
|
||||||
|
</c-button>
|
||||||
|
<c-button @click="copyEntropy">
|
||||||
|
<n-icon size="22">
|
||||||
|
<Copy />
|
||||||
|
</n-icon>
|
||||||
|
</c-button>
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
<n-form-item
|
||||||
|
label="Passphrase (mnemonic):"
|
||||||
|
:feedback="mnemonicValidation.message"
|
||||||
|
:validation-status="mnemonicValidation.status"
|
||||||
|
>
|
||||||
|
<n-input-group>
|
||||||
|
<c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text />
|
||||||
|
|
||||||
|
<c-button @click="copyPassphrase">
|
||||||
|
<n-icon size="22" :component="Copy" />
|
||||||
|
</c-button>
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
213
src/tools/camera-recorder/camera-recorder.vue
Normal file
213
src/tools/camera-recorder/camera-recorder.vue
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { useMediaRecorder } from './useMediaRecorder';
|
||||||
|
|
||||||
|
interface Media { type: 'image' | 'video'; value: string; createdAt: Date }
|
||||||
|
|
||||||
|
const {
|
||||||
|
videoInputs: cameras,
|
||||||
|
audioInputs: microphones,
|
||||||
|
permissionGranted,
|
||||||
|
isSupported,
|
||||||
|
ensurePermissions,
|
||||||
|
} = useDevicesList({
|
||||||
|
requestPermissions: true,
|
||||||
|
constraints: { video: true, audio: true },
|
||||||
|
onUpdated() {
|
||||||
|
refreshCurrentDevices();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const video = ref<HTMLVideoElement>();
|
||||||
|
const medias = ref<Media[]>([]);
|
||||||
|
const currentCamera = ref(cameras.value[0]?.deviceId);
|
||||||
|
const currentMicrophone = ref(microphones.value[0]?.deviceId);
|
||||||
|
const permissionCannotBePrompted = ref(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
stream,
|
||||||
|
start,
|
||||||
|
enabled: isMediaStreamAvailable,
|
||||||
|
} = useUserMedia({
|
||||||
|
constraints: computed(() => ({
|
||||||
|
video: { deviceId: currentCamera.value },
|
||||||
|
...(currentMicrophone.value ? { audio: { deviceId: currentMicrophone.value } } : {}),
|
||||||
|
})),
|
||||||
|
autoSwitch: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
isRecordingSupported,
|
||||||
|
onRecordAvailable,
|
||||||
|
startRecording,
|
||||||
|
stopRecording,
|
||||||
|
pauseRecording,
|
||||||
|
recordingState,
|
||||||
|
resumeRecording,
|
||||||
|
} = useMediaRecorder({
|
||||||
|
stream,
|
||||||
|
});
|
||||||
|
|
||||||
|
onRecordAvailable((value) => {
|
||||||
|
medias.value.unshift({ type: 'video', value, createdAt: new Date() });
|
||||||
|
});
|
||||||
|
|
||||||
|
function refreshCurrentDevices() {
|
||||||
|
if (_.isNil(currentCamera) || !cameras.value.find(i => i.deviceId === currentCamera.value)) {
|
||||||
|
currentCamera.value = cameras.value[0]?.deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.isNil(microphones) || !microphones.value.find(i => i.deviceId === currentMicrophone.value)) {
|
||||||
|
currentMicrophone.value = microphones.value[0]?.deviceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeScreenshot() {
|
||||||
|
if (!video.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = video.value.videoWidth;
|
||||||
|
canvas.height = video.value.videoHeight;
|
||||||
|
canvas.getContext('2d')?.drawImage(video.value, 0, 0);
|
||||||
|
const image = canvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
medias.value.unshift({ type: 'image', value: image, createdAt: new Date() });
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (video.value && stream.value) {
|
||||||
|
video.value.srcObject = stream.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function requestPermissions() {
|
||||||
|
try {
|
||||||
|
await ensurePermissions();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
permissionCannotBePrompted.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadMedia({ type, value, createdAt }: Media) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = value;
|
||||||
|
link.download = `${type}-${createdAt.getTime()}.${type === 'image' ? 'png' : 'webm'}`;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<c-card v-if="!isSupported">
|
||||||
|
Your browser does not support recording video from camera
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card v-else-if="!permissionGranted" text-center>
|
||||||
|
You need to grant permission to use your camera and microphone
|
||||||
|
|
||||||
|
<c-alert v-if="permissionCannotBePrompted" mt-4 text-left>
|
||||||
|
Your browser has blocked permission request or does not support it. You need to grant permission manually in
|
||||||
|
your browser settings (usually the lock icon in the address bar).
|
||||||
|
</c-alert>
|
||||||
|
|
||||||
|
<div v-else mt-4 flex justify-center>
|
||||||
|
<c-button @click="requestPermissions">
|
||||||
|
Grant permission
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card v-else>
|
||||||
|
<div flex gap-2>
|
||||||
|
<div flex-1>
|
||||||
|
<div>Video</div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="currentCamera"
|
||||||
|
:options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))"
|
||||||
|
placeholder="Select camera"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="currentMicrophone && microphones.length > 0" flex-1>
|
||||||
|
<div>Audio</div>
|
||||||
|
<n-select
|
||||||
|
v-model:value="currentMicrophone"
|
||||||
|
:options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))"
|
||||||
|
placeholder="Select microphone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isMediaStreamAvailable" mt-3 flex justify-center>
|
||||||
|
<c-button type="primary" @click="start">
|
||||||
|
Start webcam
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div my-2>
|
||||||
|
<video ref="video" autoplay controls playsinline max-h-full w-full />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div flex items-center justify-between gap-2>
|
||||||
|
<c-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot">
|
||||||
|
<span mr-2> <icon-mdi-camera /></span>
|
||||||
|
Take screenshot
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<div v-if="isRecordingSupported" flex justify-center gap-2>
|
||||||
|
<c-button v-if="recordingState === 'stopped'" @click="startRecording">
|
||||||
|
<span mr-2> <icon-mdi-video /></span>
|
||||||
|
Start recording
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<c-button v-if="recordingState === 'recording'" @click="pauseRecording">
|
||||||
|
<span mr-2> <icon-mdi-pause /></span>
|
||||||
|
Pause
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<c-button v-if="recordingState === 'paused'" @click="resumeRecording">
|
||||||
|
<span mr-2> <icon-mdi-play /></span>
|
||||||
|
Resume
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<c-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording">
|
||||||
|
<span mr-2> <icon-mdi-record /></span>
|
||||||
|
Stop
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
<div v-else italic op-60>
|
||||||
|
Video recording is not supported in your browser
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<div grid grid-cols-2 mt-5 gap-2>
|
||||||
|
<c-card v-for="({ type, value, createdAt }, index) in medias" :key="index">
|
||||||
|
<img v-if="type === 'image'" :src="value" max-h-full w-full alt="screenshot">
|
||||||
|
|
||||||
|
<video v-else :src="value" controls max-h-full w-full />
|
||||||
|
|
||||||
|
<div flex items-center justify-between>
|
||||||
|
<div font-bold>
|
||||||
|
{{ type === 'image' ? 'Screenshot' : 'Video' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div flex gap-2>
|
||||||
|
<c-button @click="downloadMedia({ type, value, createdAt })">
|
||||||
|
<icon-mdi-download />
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<c-button @click="medias = medias.filter((_ignored, i) => i !== index)">
|
||||||
|
<icon-mdi-delete-outline />
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
12
src/tools/camera-recorder/index.ts
Normal file
12
src/tools/camera-recorder/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { Camera } from '@vicons/tabler';
|
||||||
|
import { defineTool } from '../tool';
|
||||||
|
|
||||||
|
export const tool = defineTool({
|
||||||
|
name: 'Camera recorder',
|
||||||
|
path: '/camera-recorder',
|
||||||
|
description: 'Take a picture or record a video from your webcam or camera.',
|
||||||
|
keywords: ['camera', 'recoder'],
|
||||||
|
component: () => import('./camera-recorder.vue'),
|
||||||
|
icon: Camera,
|
||||||
|
createdAt: new Date('2023-05-15'),
|
||||||
|
});
|
114
src/tools/camera-recorder/useMediaRecorder.ts
Normal file
114
src/tools/camera-recorder/useMediaRecorder.ts
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import { type Ref, computed, ref } from 'vue';
|
||||||
|
|
||||||
|
export { useMediaRecorder };
|
||||||
|
|
||||||
|
function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): {
|
||||||
|
isRecordingSupported: Ref<boolean>
|
||||||
|
recordingState: Ref<'stopped' | 'recording' | 'paused'>
|
||||||
|
startRecording: () => void
|
||||||
|
stopRecording: () => void
|
||||||
|
pauseRecording: () => void
|
||||||
|
resumeRecording: () => void
|
||||||
|
onRecordAvailable: (cb: (url: string) => void) => void
|
||||||
|
} {
|
||||||
|
const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm'));
|
||||||
|
const mediaRecorder = ref<MediaRecorder | null>(null);
|
||||||
|
const recordedChunks = ref<Blob[]>([]);
|
||||||
|
const recordAvailable = createEventHook();
|
||||||
|
const recordingState = ref<'stopped' | 'recording' | 'paused'>('stopped');
|
||||||
|
|
||||||
|
const createVideo = () => {
|
||||||
|
const blob = new Blob(recordedChunks.value, { type: 'video/webm' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
recordedChunks.value = [];
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRecording = () => {
|
||||||
|
if (!isRecordingSupported.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!stream.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recordingState.value !== 'stopped') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' });
|
||||||
|
|
||||||
|
mediaRecorder.value.ondataavailable = (e) => {
|
||||||
|
if (e.data.size > 0) {
|
||||||
|
recordedChunks.value.push(e.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.value.onstop = () => {
|
||||||
|
recordAvailable.trigger(createVideo());
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mediaRecorder.value.state !== 'inactive') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorder.value.start();
|
||||||
|
recordingState.value = 'recording';
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRecording = () => {
|
||||||
|
if (!isRecordingSupported.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mediaRecorder.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recordingState.value === 'stopped') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorder.value.stop();
|
||||||
|
recordingState.value = 'stopped';
|
||||||
|
};
|
||||||
|
|
||||||
|
const pauseRecording = () => {
|
||||||
|
if (!isRecordingSupported.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mediaRecorder.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recordingState.value !== 'recording') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorder.value.pause();
|
||||||
|
recordingState.value = 'paused';
|
||||||
|
};
|
||||||
|
|
||||||
|
const resumeRecording = () => {
|
||||||
|
if (!isRecordingSupported.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mediaRecorder.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recordingState.value !== 'paused') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorder.value.resume();
|
||||||
|
recordingState.value = 'recording';
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isRecordingSupported,
|
||||||
|
startRecording,
|
||||||
|
stopRecording,
|
||||||
|
pauseRecording,
|
||||||
|
resumeRecording,
|
||||||
|
recordingState,
|
||||||
|
|
||||||
|
onRecordAvailable: recordAvailable.on,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,49 +1,3 @@
|
||||||
<template>
|
|
||||||
<n-card>
|
|
||||||
<n-form label-width="120" label-placement="left" :show-feedback="false">
|
|
||||||
<n-form-item label="Your string:">
|
|
||||||
<n-input v-model:value="input" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-divider />
|
|
||||||
|
|
||||||
<n-form-item label="Camelcase:">
|
|
||||||
<input-copyable :value="camelCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Capitalcase:">
|
|
||||||
<input-copyable :value="capitalCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Constantcase:">
|
|
||||||
<input-copyable :value="constantCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Dotcase:">
|
|
||||||
<input-copyable :value="dotCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Headercase:">
|
|
||||||
<input-copyable :value="headerCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Nocase:">
|
|
||||||
<input-copyable :value="noCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Paramcase:">
|
|
||||||
<input-copyable :value="paramCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Pascalcase:">
|
|
||||||
<input-copyable :value="pascalCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Pathcase:">
|
|
||||||
<input-copyable :value="pathCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Sentencecase:">
|
|
||||||
<input-copyable :value="sentenceCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Snakecase:">
|
|
||||||
<input-copyable :value="snakeCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import {
|
import {
|
||||||
|
@ -68,6 +22,58 @@ const baseConfig = {
|
||||||
const input = ref('lorem ipsum dolor sit amet');
|
const input = ref('lorem ipsum dolor sit amet');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-card>
|
||||||
|
<n-form label-width="120" label-placement="left" :show-feedback="false">
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="input"
|
||||||
|
label="Your string"
|
||||||
|
label-position="left"
|
||||||
|
label-width="120px"
|
||||||
|
label-align="right"
|
||||||
|
placeholder="Your string..."
|
||||||
|
raw-text
|
||||||
|
/>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<n-form-item label="Camelcase:">
|
||||||
|
<InputCopyable :value="camelCase(input, baseConfig)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Capitalcase:">
|
||||||
|
<InputCopyable :value="capitalCase(input, baseConfig)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Constantcase:">
|
||||||
|
<InputCopyable :value="constantCase(input, baseConfig)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Dotcase:">
|
||||||
|
<InputCopyable :value="dotCase(input, baseConfig)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Headercase:">
|
||||||
|
<InputCopyable :value="headerCase(input, baseConfig)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Nocase:">
|
||||||
|
<InputCopyable :value="noCase(input, baseConfig)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Paramcase:">
|
||||||
|
<InputCopyable :value="paramCase(input, baseConfig)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Pascalcase:">
|
||||||
|
<InputCopyable :value="pascalCase(input, baseConfig)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Pathcase:">
|
||||||
|
<InputCopyable :value="pathCase(input, baseConfig)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Sentencecase:">
|
||||||
|
<InputCopyable :value="sentenceCase(input, baseConfig)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Snakecase:">
|
||||||
|
<InputCopyable :value="snakeCase(input, baseConfig)" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.n-form-item {
|
.n-form-item {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { expect, describe, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
|
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
|
||||||
|
|
||||||
describe('chmod-calculator', () => {
|
describe('chmod-calculator', () => {
|
||||||
|
|
|
@ -1,38 +1,8 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="text-center" scope="col"></th>
|
|
||||||
<th class="text-center" scope="col">Owner (u)</th>
|
|
||||||
<th class="text-center" scope="col">Group (g)</th>
|
|
||||||
<th class="text-center" scope="col">Public (o)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="{ scope, title } of scopes" :key="scope">
|
|
||||||
<td class="line-header">{{ title }}</td>
|
|
||||||
<td v-for="group of groups" :key="group" class="text-center">
|
|
||||||
<!-- <n-switch v-model:value="permissions[group][scope]" /> -->
|
|
||||||
<n-checkbox v-model:checked="permissions[group][scope]" size="large" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</n-table>
|
|
||||||
|
|
||||||
<div class="octal-result">
|
|
||||||
{{ octal }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input-copyable :value="`chmod ${octal} path`" readonly />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useThemeVars } from 'naive-ui';
|
import { useThemeVars } from 'naive-ui';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
|
|
||||||
import InputCopyable from '../../components/InputCopyable.vue';
|
import InputCopyable from '../../components/InputCopyable.vue';
|
||||||
|
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
|
||||||
|
|
||||||
import type { Group, Scope } from './chmod-calculator.types';
|
import type { Group, Scope } from './chmod-calculator.types';
|
||||||
|
|
||||||
|
@ -54,6 +24,44 @@ const permissions = ref({
|
||||||
const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
|
const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center" scope="col" />
|
||||||
|
<th class="text-center" scope="col">
|
||||||
|
Owner (u)
|
||||||
|
</th>
|
||||||
|
<th class="text-center" scope="col">
|
||||||
|
Group (g)
|
||||||
|
</th>
|
||||||
|
<th class="text-center" scope="col">
|
||||||
|
Public (o)
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="{ scope, title } of scopes" :key="scope">
|
||||||
|
<td class="line-header">
|
||||||
|
{{ title }}
|
||||||
|
</td>
|
||||||
|
<td v-for="group of groups" :key="group" class="text-center">
|
||||||
|
<!-- <n-switch v-model:value="permissions[group][scope]" /> -->
|
||||||
|
<n-checkbox v-model:checked="permissions[group][scope]" size="large" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</n-table>
|
||||||
|
|
||||||
|
<div class="octal-result">
|
||||||
|
{{ octal }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InputCopyable :value="`chmod ${octal} path`" readonly />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.octal-result {
|
.octal-result {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -1,18 +1,3 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-card>
|
|
||||||
<div class="duration">{{ formatMs(counter) }}</div>
|
|
||||||
</n-card>
|
|
||||||
<br />
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button v-if="!isRunning" secondary type="primary" @click="resume">Start</n-button>
|
|
||||||
<n-button v-else secondary type="warning" @click="pause">Stop</n-button>
|
|
||||||
|
|
||||||
<n-button secondary @click="counter = 0">Reset</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRafFn } from '@vueuse/core';
|
import { useRafFn } from '@vueuse/core';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
@ -43,6 +28,28 @@ function pause() {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<c-card>
|
||||||
|
<div class="duration">
|
||||||
|
{{ formatMs(counter) }}
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
<div mt-5 flex justify-center gap-3>
|
||||||
|
<c-button v-if="!isRunning" type="primary" @click="resume">
|
||||||
|
Start
|
||||||
|
</c-button>
|
||||||
|
<c-button v-else type="warning" @click="pause">
|
||||||
|
Stop
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<c-button @click="counter = 0">
|
||||||
|
Reset
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.duration {
|
.duration {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -1,38 +1,3 @@
|
||||||
<template>
|
|
||||||
<n-card>
|
|
||||||
<n-form label-width="100" label-placement="left">
|
|
||||||
<n-form-item label="color picker:">
|
|
||||||
<n-color-picker
|
|
||||||
v-model:value="hex"
|
|
||||||
placement="bottom-end"
|
|
||||||
@update:value="(v: string) => onInputUpdated(v, 'hex')"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="color name:">
|
|
||||||
<input-copyable v-model:value="name" :on-input="(v: string) => onInputUpdated(v, 'name')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="hex:">
|
|
||||||
<input-copyable v-model:value="hex" :on-input="(v: string) => onInputUpdated(v, 'hex')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="rgb:">
|
|
||||||
<input-copyable v-model:value="rgb" :on-input="(v: string) => onInputUpdated(v, 'rgb')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="hsl:">
|
|
||||||
<input-copyable v-model:value="hsl" :on-input="(v: string) => onInputUpdated(v, 'hsl')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="hwb:">
|
|
||||||
<input-copyable v-model:value="hwb" :on-input="(v: string) => onInputUpdated(v, 'hwb')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="lch:">
|
|
||||||
<input-copyable v-model:value="lch" :on-input="(v: string) => onInputUpdated(v, 'lch')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="cmyk:">
|
|
||||||
<input-copyable v-model:value="cmyk" :on-input="(v: string) => onInputUpdated(v, 'cmyk')" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { colord, extend } from 'colord';
|
import { colord, extend } from 'colord';
|
||||||
|
@ -54,16 +19,70 @@ const cmyk = ref('');
|
||||||
const lch = ref('');
|
const lch = ref('');
|
||||||
|
|
||||||
function onInputUpdated(value: string, omit: string) {
|
function onInputUpdated(value: string, omit: string) {
|
||||||
|
try {
|
||||||
const color = colord(value);
|
const color = colord(value);
|
||||||
|
|
||||||
if (omit !== 'name') name.value = color.toName({ closest: true }) ?? '';
|
if (omit !== 'name') {
|
||||||
if (omit !== 'hex') hex.value = color.toHex();
|
name.value = color.toName({ closest: true }) ?? '';
|
||||||
if (omit !== 'rgb') rgb.value = color.toRgbString();
|
}
|
||||||
if (omit !== 'hsl') hsl.value = color.toHslString();
|
if (omit !== 'hex') {
|
||||||
if (omit !== 'hwb') hwb.value = color.toHwbString();
|
hex.value = color.toHex();
|
||||||
if (omit !== 'cmyk') cmyk.value = color.toCmykString();
|
}
|
||||||
if (omit !== 'lch') lch.value = color.toLchString();
|
if (omit !== 'rgb') {
|
||||||
|
rgb.value = color.toRgbString();
|
||||||
|
}
|
||||||
|
if (omit !== 'hsl') {
|
||||||
|
hsl.value = color.toHslString();
|
||||||
|
}
|
||||||
|
if (omit !== 'hwb') {
|
||||||
|
hwb.value = color.toHwbString();
|
||||||
|
}
|
||||||
|
if (omit !== 'cmyk') {
|
||||||
|
cmyk.value = color.toCmykString();
|
||||||
|
}
|
||||||
|
if (omit !== 'lch') {
|
||||||
|
lch.value = color.toLchString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputUpdated(hex.value, 'hex');
|
onInputUpdated(hex.value, 'hex');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-card>
|
||||||
|
<n-form label-width="100" label-placement="left">
|
||||||
|
<n-form-item label="color picker:">
|
||||||
|
<n-color-picker
|
||||||
|
v-model:value="hex"
|
||||||
|
placement="bottom-end"
|
||||||
|
@update:value="(v: string) => onInputUpdated(v, 'hex')"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="color name:">
|
||||||
|
<InputCopyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="hex:">
|
||||||
|
<InputCopyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="rgb:">
|
||||||
|
<InputCopyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="hsl:">
|
||||||
|
<InputCopyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="hwb:">
|
||||||
|
<InputCopyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="lch:">
|
||||||
|
<InputCopyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="cmyk:">
|
||||||
|
<InputCopyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,92 +1,7 @@
|
||||||
<template>
|
|
||||||
<n-card>
|
|
||||||
<n-form-item
|
|
||||||
class="cron"
|
|
||||||
:show-label="false"
|
|
||||||
:feedback="cronValidation.message"
|
|
||||||
:validation-status="cronValidation.status"
|
|
||||||
>
|
|
||||||
<n-input v-model:value="cron" size="large" placeholder="* * * * *" />
|
|
||||||
</n-form-item>
|
|
||||||
<div class="cron-string">
|
|
||||||
{{ cronString }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-divider />
|
|
||||||
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-form :show-feedback="false" label-width="170" label-placement="left">
|
|
||||||
<n-form-item label="Verbose">
|
|
||||||
<n-switch v-model:value="cronstrueConfig.verbose" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Use 24 hour time format">
|
|
||||||
<n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Days start at 0">
|
|
||||||
<n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
<n-card>
|
|
||||||
<pre>
|
|
||||||
┌──────────── [optional] seconds (0 - 59)
|
|
||||||
| ┌────────── minute (0 - 59)
|
|
||||||
| | ┌──────── hour (0 - 23)
|
|
||||||
| | | ┌────── day of month (1 - 31)
|
|
||||||
| | | | ┌──── month (1 - 12) OR jan,feb,mar,apr ...
|
|
||||||
| | | | | ┌── day of week (0 - 6, sunday=0) OR sun,mon ...
|
|
||||||
| | | | | |
|
|
||||||
* * * * * * command</pre
|
|
||||||
>
|
|
||||||
|
|
||||||
<n-space v-if="styleStore.isSmallScreen" vertical>
|
|
||||||
<n-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" embedded :bordered="false">
|
|
||||||
<div>
|
|
||||||
Symbol: <strong>{{ symbol }}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Meaning: <strong>{{ meaning }}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Example:
|
|
||||||
<strong
|
|
||||||
><code>{{ example }}</code></strong
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Equivalent: <strong>{{ equivalent }}</strong>
|
|
||||||
</div>
|
|
||||||
</n-card>
|
|
||||||
</n-space>
|
|
||||||
<n-table v-else size="small">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="text-left" scope="col">Symbol</th>
|
|
||||||
<th class="text-left" scope="col">Meaning</th>
|
|
||||||
<th class="text-left" scope="col">Example</th>
|
|
||||||
<th class="text-left" scope="col">Equivalent</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol">
|
|
||||||
<td>{{ symbol }}</td>
|
|
||||||
<td>{{ meaning }}</td>
|
|
||||||
<td>
|
|
||||||
<code>{{ example }}</code>
|
|
||||||
</td>
|
|
||||||
<td>{{ equivalent }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</n-table>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import cronstrue from 'cronstrue';
|
import cronstrue from 'cronstrue';
|
||||||
import { isValidCron } from 'cron-validator';
|
import { isValidCron } from 'cron-validator';
|
||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, reactive, ref } from 'vue';
|
||||||
import { useValidation } from '@/composable/validation';
|
|
||||||
import { useStyleStore } from '@/stores/style.store';
|
import { useStyleStore } from '@/stores/style.store';
|
||||||
|
|
||||||
function isCronValid(v: string) {
|
function isCronValid(v: string) {
|
||||||
|
@ -185,30 +100,111 @@ const cronString = computed(() => {
|
||||||
return ' ';
|
return ' ';
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronValidation = useValidation({
|
const cronValidationRules = [
|
||||||
source: cron,
|
|
||||||
rules: [
|
|
||||||
{
|
{
|
||||||
validator: (value) => isCronValid(value),
|
validator: (value: string) => isCronValid(value),
|
||||||
message: 'This cron is invalid',
|
message: 'This cron is invalid',
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-card>
|
||||||
|
<div mx-auto max-w-sm>
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="cron"
|
||||||
|
size="large"
|
||||||
|
placeholder="* * * * *"
|
||||||
|
:validation-rules="cronValidationRules"
|
||||||
|
mb-3
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cron-string">
|
||||||
|
{{ cronString }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<n-form :show-feedback="false" label-width="170" label-placement="left">
|
||||||
|
<n-form-item label="Verbose">
|
||||||
|
<n-switch v-model:value="cronstrueConfig.verbose" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Use 24 hour time format">
|
||||||
|
<n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Days start at 0">
|
||||||
|
<n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
<c-card>
|
||||||
|
<pre>
|
||||||
|
┌──────────── [optional] seconds (0 - 59)
|
||||||
|
| ┌────────── minute (0 - 59)
|
||||||
|
| | ┌──────── hour (0 - 23)
|
||||||
|
| | | ┌────── day of month (1 - 31)
|
||||||
|
| | | | ┌──── month (1 - 12) OR jan,feb,mar,apr ...
|
||||||
|
| | | | | ┌── day of week (0 - 6, sunday=0) OR sun,mon ...
|
||||||
|
| | | | | |
|
||||||
|
* * * * * * command</pre>
|
||||||
|
|
||||||
|
<div v-if="styleStore.isSmallScreen">
|
||||||
|
<c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none>
|
||||||
|
<div>
|
||||||
|
Symbol: <strong>{{ symbol }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Meaning: <strong>{{ meaning }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Example:
|
||||||
|
<strong><code>{{ example }}</code></strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Equivalent: <strong>{{ equivalent }}</strong>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
</div>
|
||||||
|
<n-table v-else size="small">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left" scope="col">
|
||||||
|
Symbol
|
||||||
|
</th>
|
||||||
|
<th class="text-left" scope="col">
|
||||||
|
Meaning
|
||||||
|
</th>
|
||||||
|
<th class="text-left" scope="col">
|
||||||
|
Example
|
||||||
|
</th>
|
||||||
|
<th class="text-left" scope="col">
|
||||||
|
Equivalent
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol">
|
||||||
|
<td>{{ symbol }}</td>
|
||||||
|
<td>{{ meaning }}</td>
|
||||||
|
<td>
|
||||||
|
<code>{{ example }}</code>
|
||||||
|
</td>
|
||||||
|
<td>{{ equivalent }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</n-table>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.cron {
|
::v-deep(input) {
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
max-width: 400px;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
.n-input {
|
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cron-string {
|
.cron-string {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
test.describe('Date time converter - json to yaml', () => {
|
test.describe('Date time converter - json to yaml', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { describe, test, expect } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
import {
|
import {
|
||||||
isISO8601DateTimeString,
|
isISO8601DateTimeString,
|
||||||
isISO9075DateString,
|
isISO9075DateString,
|
||||||
|
isMongoObjectId,
|
||||||
isRFC3339DateString,
|
isRFC3339DateString,
|
||||||
isRFC7231DateString,
|
isRFC7231DateString,
|
||||||
isUnixTimestamp,
|
|
||||||
isTimestamp,
|
isTimestamp,
|
||||||
isUTCDateString,
|
isUTCDateString,
|
||||||
isMongoObjectId,
|
isUnixTimestamp,
|
||||||
} from './date-time-converter.models';
|
} from './date-time-converter.models';
|
||||||
|
|
||||||
describe('date-time-converter models', () => {
|
describe('date-time-converter models', () => {
|
||||||
|
|
|
@ -11,13 +11,13 @@ export {
|
||||||
isMongoObjectId,
|
isMongoObjectId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ISO8601_REGEX =
|
const ISO8601_REGEX
|
||||||
/^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
|
= /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
|
||||||
const ISO9075_REGEX =
|
const ISO9075_REGEX
|
||||||
/^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/;
|
= /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/;
|
||||||
|
|
||||||
const RFC3339_REGEX =
|
const RFC3339_REGEX
|
||||||
/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/;
|
= /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/;
|
||||||
|
|
||||||
const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/;
|
const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/;
|
||||||
|
|
||||||
|
@ -40,7 +40,8 @@ function isUTCDateString(date?: string) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return new Date(date).toUTCString() === date;
|
return new Date(date).toUTCString() === date;
|
||||||
} catch (_ignored) {
|
}
|
||||||
|
catch (_ignored) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
export type ToDateMapper = (value: string) => Date;
|
export type ToDateMapper = (value: string) => Date;
|
||||||
|
|
||||||
export type DateFormat = {
|
export interface DateFormat {
|
||||||
name: string;
|
name: string
|
||||||
fromDate: (date: Date) => string;
|
fromDate: (date: Date) => string
|
||||||
toDate: (value: string) => Date;
|
toDate: (value: string) => Date
|
||||||
formatMatcher: (dateString: string) => boolean;
|
formatMatcher: (dateString: string) => boolean
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,37 +1,3 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-form-item :show-label="false" v-bind="validation.attrs">
|
|
||||||
<n-input-group>
|
|
||||||
<n-input
|
|
||||||
v-model:value="inputDate"
|
|
||||||
:on-input="onDateInputChanged"
|
|
||||||
placeholder="Put you date string here..."
|
|
||||||
clearable
|
|
||||||
:input-props="{ 'data-test-id': 'date-time-converter-input' }"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<n-select
|
|
||||||
v-model:value="formatIndex"
|
|
||||||
style="flex: 0 0 170px"
|
|
||||||
:options="formats.map(({ name }, i) => ({ label: name, value: i }))"
|
|
||||||
data-test-id="date-time-converter-format-select"
|
|
||||||
/>
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item>
|
|
||||||
<n-divider style="margin-top: 0" />
|
|
||||||
<div v-for="{ name, fromDate } in formats" :key="name" mt-1>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label style="flex: 0 0 170px"> {{ name }}: </n-input-group-label>
|
|
||||||
<input-copyable
|
|
||||||
:value="formatDateUsingFormatter(fromDate, normalizedDate)"
|
|
||||||
placeholder="Invalid date..."
|
|
||||||
:input-props="{ 'data-test-id': name }"
|
|
||||||
/>
|
|
||||||
</n-input-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
formatISO,
|
formatISO,
|
||||||
|
@ -41,41 +7,33 @@ import {
|
||||||
fromUnixTime,
|
fromUnixTime,
|
||||||
getTime,
|
getTime,
|
||||||
getUnixTime,
|
getUnixTime,
|
||||||
parseISO,
|
|
||||||
parseJSON,
|
|
||||||
isDate,
|
isDate,
|
||||||
isValid,
|
isValid,
|
||||||
|
parseISO,
|
||||||
|
parseJSON,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { withDefaultOnError } from '@/utils/defaults';
|
|
||||||
import { useValidation } from '@/composable/validation';
|
|
||||||
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
|
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
|
||||||
import {
|
import {
|
||||||
isISO8601DateTimeString,
|
isISO8601DateTimeString,
|
||||||
isISO9075DateString,
|
isISO9075DateString,
|
||||||
|
isMongoObjectId,
|
||||||
isRFC3339DateString,
|
isRFC3339DateString,
|
||||||
isRFC7231DateString,
|
isRFC7231DateString,
|
||||||
isTimestamp,
|
isTimestamp,
|
||||||
isUTCDateString,
|
isUTCDateString,
|
||||||
isUnixTimestamp,
|
isUnixTimestamp,
|
||||||
isMongoObjectId,
|
|
||||||
} from './date-time-converter.models';
|
} from './date-time-converter.models';
|
||||||
|
import { withDefaultOnError } from '@/utils/defaults';
|
||||||
|
import { useValidation } from '@/composable/validation';
|
||||||
|
|
||||||
const inputDate = ref('');
|
const inputDate = ref('');
|
||||||
|
|
||||||
const toDate: ToDateMapper = (date) => new Date(date);
|
const toDate: ToDateMapper = date => new Date(date);
|
||||||
|
|
||||||
function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) {
|
|
||||||
if (!date || !validation.isValid) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return withDefaultOnError(() => formatter(date), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
const formats: DateFormat[] = [
|
const formats: DateFormat[] = [
|
||||||
{
|
{
|
||||||
name: 'JS locale date string',
|
name: 'JS locale date string',
|
||||||
fromDate: (date) => date.toString(),
|
fromDate: date => date.toString(),
|
||||||
toDate,
|
toDate,
|
||||||
formatMatcher: () => false,
|
formatMatcher: () => false,
|
||||||
},
|
},
|
||||||
|
@ -83,49 +41,49 @@ const formats: DateFormat[] = [
|
||||||
name: 'ISO 8601',
|
name: 'ISO 8601',
|
||||||
fromDate: formatISO,
|
fromDate: formatISO,
|
||||||
toDate: parseISO,
|
toDate: parseISO,
|
||||||
formatMatcher: (date) => isISO8601DateTimeString(date),
|
formatMatcher: date => isISO8601DateTimeString(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ISO 9075',
|
name: 'ISO 9075',
|
||||||
fromDate: formatISO9075,
|
fromDate: formatISO9075,
|
||||||
toDate: parseISO,
|
toDate: parseISO,
|
||||||
formatMatcher: (date) => isISO9075DateString(date),
|
formatMatcher: date => isISO9075DateString(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'RFC 3339',
|
name: 'RFC 3339',
|
||||||
fromDate: formatRFC3339,
|
fromDate: formatRFC3339,
|
||||||
toDate,
|
toDate,
|
||||||
formatMatcher: (date) => isRFC3339DateString(date),
|
formatMatcher: date => isRFC3339DateString(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'RFC 7231',
|
name: 'RFC 7231',
|
||||||
fromDate: formatRFC7231,
|
fromDate: formatRFC7231,
|
||||||
toDate,
|
toDate,
|
||||||
formatMatcher: (date) => isRFC7231DateString(date),
|
formatMatcher: date => isRFC7231DateString(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Unix timestamp',
|
name: 'Unix timestamp',
|
||||||
fromDate: (date) => String(getUnixTime(date)),
|
fromDate: date => String(getUnixTime(date)),
|
||||||
toDate: (sec) => fromUnixTime(+sec),
|
toDate: sec => fromUnixTime(+sec),
|
||||||
formatMatcher: (date) => isUnixTimestamp(date),
|
formatMatcher: date => isUnixTimestamp(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Timestamp',
|
name: 'Timestamp',
|
||||||
fromDate: (date) => String(getTime(date)),
|
fromDate: date => String(getTime(date)),
|
||||||
toDate: (ms) => parseJSON(+ms),
|
toDate: ms => parseJSON(+ms),
|
||||||
formatMatcher: (date) => isTimestamp(date),
|
formatMatcher: date => isTimestamp(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'UTC format',
|
name: 'UTC format',
|
||||||
fromDate: (date) => date.toUTCString(),
|
fromDate: date => date.toUTCString(),
|
||||||
toDate,
|
toDate,
|
||||||
formatMatcher: (date) => isUTCDateString(date),
|
formatMatcher: date => isUTCDateString(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Mongo ObjectID',
|
name: 'Mongo ObjectID',
|
||||||
fromDate: (date) => Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000',
|
fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`,
|
||||||
toDate: (objectId) => new Date(parseInt(objectId.substring(0, 8), 16) * 1000),
|
toDate: objectId => new Date(parseInt(objectId.substring(0, 8), 16) * 1000),
|
||||||
formatMatcher: (date) => isMongoObjectId(date),
|
formatMatcher: date => isMongoObjectId(date),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -141,7 +99,8 @@ const normalizedDate = computed(() => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return toDate(inputDate.value);
|
return toDate(inputDate.value);
|
||||||
} catch (_ignored) {
|
}
|
||||||
|
catch (_ignored) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -159,9 +118,11 @@ const validation = useValidation({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
message: 'This date is invalid for this format',
|
message: 'This date is invalid for this format',
|
||||||
validator: (value) =>
|
validator: value =>
|
||||||
withDefaultOnError(() => {
|
withDefaultOnError(() => {
|
||||||
if (value === '') return true;
|
if (value === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const maybeDate = formats[formatIndex.value].toDate(value);
|
const maybeDate = formats[formatIndex.value].toDate(value);
|
||||||
return isDate(maybeDate) && isValid(maybeDate);
|
return isDate(maybeDate) && isValid(maybeDate);
|
||||||
|
@ -169,4 +130,51 @@ const validation = useValidation({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) {
|
||||||
|
if (!date || !validation.isValid) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return withDefaultOnError(() => formatter(date), '');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-input-group>
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="inputDate"
|
||||||
|
autofocus
|
||||||
|
placeholder="Put you date string here..."
|
||||||
|
clearable
|
||||||
|
test-id="date-time-converter-input"
|
||||||
|
:validation="validation"
|
||||||
|
@update:value="onDateInputChanged"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<n-select
|
||||||
|
v-model:value="formatIndex"
|
||||||
|
style="flex: 0 0 170px"
|
||||||
|
:options="formats.map(({ name }, i) => ({ label: name, value: i }))"
|
||||||
|
data-test-id="date-time-converter-format-select"
|
||||||
|
/>
|
||||||
|
</n-input-group>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<input-copyable
|
||||||
|
v-for="{ name, fromDate } in formats"
|
||||||
|
:key="name"
|
||||||
|
:label="name"
|
||||||
|
label-width="150px"
|
||||||
|
label-position="left"
|
||||||
|
label-align="right"
|
||||||
|
:value="formatDateUsingFormatter(fromDate, normalizedDate)"
|
||||||
|
placeholder="Invalid date..."
|
||||||
|
:test-id="name"
|
||||||
|
readonly
|
||||||
|
mt-2
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,24 +1,3 @@
|
||||||
<template>
|
|
||||||
<n-card v-for="{ name, information } in sections" :key="name" :title="name">
|
|
||||||
<n-grid cols="1 400:2" x-gap="12" y-gap="12">
|
|
||||||
<n-gi v-for="{ label, value: { value } } in information" :key="label" class="information">
|
|
||||||
<n-card :bordered="false" embedded>
|
|
||||||
<div class="label">
|
|
||||||
{{ label }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="value">
|
|
||||||
<n-ellipsis v-if="value">
|
|
||||||
{{ value }}
|
|
||||||
</n-ellipsis>
|
|
||||||
<div v-else class="undefined-value">unknown</div>
|
|
||||||
</div>
|
|
||||||
</n-card>
|
|
||||||
</n-gi>
|
|
||||||
</n-grid>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useWindowSize } from '@vueuse/core';
|
import { useWindowSize } from '@vueuse/core';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
@ -79,8 +58,33 @@ const sections = [
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-card v-for="{ name, information } in sections" :key="name" :title="name">
|
||||||
|
<n-grid cols="1 400:2" x-gap="12" y-gap="12">
|
||||||
|
<n-gi v-for="{ label, value: { value } } in information" :key="label" class="information">
|
||||||
|
<div class="label">
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value">
|
||||||
|
<n-ellipsis v-if="value">
|
||||||
|
{{ value }}
|
||||||
|
</n-ellipsis>
|
||||||
|
<div v-else class="undefined-value">
|
||||||
|
unknown
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.information {
|
.information {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #aaaaaa11;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
|
|
@ -1,3 +1,34 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { MessageType, composerize } from 'composerize-ts';
|
||||||
|
import { withDefaultOnError } from '@/utils/defaults';
|
||||||
|
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||||
|
import { textToBase64 } from '@/utils/base64';
|
||||||
|
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||||
|
|
||||||
|
const dockerRun = ref(
|
||||||
|
'docker run -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro --restart always --log-opt max-size=1g nginx',
|
||||||
|
);
|
||||||
|
|
||||||
|
const conversionResult = computed(() =>
|
||||||
|
withDefaultOnError(() => composerize(dockerRun.value), { yaml: '', messages: [] }),
|
||||||
|
);
|
||||||
|
const dockerCompose = computed(() => conversionResult.value.yaml);
|
||||||
|
const notImplemented = computed(() =>
|
||||||
|
conversionResult.value.messages.filter(msg => msg.type === MessageType.notImplemented).map(msg => msg.value),
|
||||||
|
);
|
||||||
|
const notComposable = computed(() =>
|
||||||
|
conversionResult.value.messages.filter(msg => msg.type === MessageType.notTranslatable).map(msg => msg.value),
|
||||||
|
);
|
||||||
|
const errors = computed(() =>
|
||||||
|
conversionResult.value.messages
|
||||||
|
.filter(msg => msg.type === MessageType.errorDuringConversion)
|
||||||
|
.map(msg => msg.value),
|
||||||
|
);
|
||||||
|
const dockerComposeBase64 = computed(() => `data:application/yaml;base64,${textToBase64(dockerCompose.value)}`);
|
||||||
|
const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' });
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-form-item label="Your docker run command:" :show-feedback="false">
|
<n-form-item label="Your docker run command:" :show-feedback="false">
|
||||||
|
@ -12,73 +43,46 @@
|
||||||
|
|
||||||
<n-divider />
|
<n-divider />
|
||||||
|
|
||||||
<textarea-copyable :value="dockerCompose" language="yaml" />
|
<TextareaCopyable :value="dockerCompose" language="yaml" />
|
||||||
<br />
|
|
||||||
<br />
|
<div mt-5 flex justify-center>
|
||||||
<n-space justify="center">
|
<c-button :disabled="dockerCompose === ''" secondary @click="download">
|
||||||
<n-button :disabled="dockerCompose === ''" secondary @click="download"> Download docker-compose.yml </n-button>
|
Download docker-compose.yml
|
||||||
</n-space>
|
</c-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="notComposable.length > 0">
|
<div v-if="notComposable.length > 0">
|
||||||
<br />
|
<n-alert title="This options are not translatable to docker-compose" type="info" mt-5>
|
||||||
<n-alert title="This options are not translatable to docker-compose" type="info">
|
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(message, index) of notComposable" :key="index">{{ message }}</li>
|
<li v-for="(message, index) of notComposable" :key="index">
|
||||||
|
{{ message }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="notImplemented.length > 0">
|
<div v-if="notImplemented.length > 0">
|
||||||
<br />
|
|
||||||
<n-alert
|
<n-alert
|
||||||
title="This options are not yet implemented and therefore haven't been translated to docker-compose"
|
title="This options are not yet implemented and therefore haven't been translated to docker-compose"
|
||||||
type="warning"
|
type="warning"
|
||||||
|
mt-5
|
||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li>
|
<li v-for="(message, index) of notImplemented" :key="index">
|
||||||
|
{{ message }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="errors.length > 0">
|
<div v-if="errors.length > 0">
|
||||||
<br />
|
<n-alert title="The following errors occured" type="error" mt-5>
|
||||||
<n-alert title="The following errors occured" type="error">
|
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(message, index) of errors" :key="index">{{ message }}</li>
|
<li v-for="(message, index) of errors" :key="index">
|
||||||
|
{{ message }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { withDefaultOnError } from '@/utils/defaults';
|
|
||||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
|
||||||
import { textToBase64 } from '@/utils/base64';
|
|
||||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
|
||||||
|
|
||||||
import { composerize, MessageType } from 'composerize-ts';
|
|
||||||
|
|
||||||
const dockerRun = ref(
|
|
||||||
'docker run -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro --restart always --log-opt max-size=1g nginx',
|
|
||||||
);
|
|
||||||
|
|
||||||
const conversionResult = computed(() =>
|
|
||||||
withDefaultOnError(() => composerize(dockerRun.value), { yaml: '', messages: [] }),
|
|
||||||
);
|
|
||||||
const dockerCompose = computed(() => conversionResult.value.yaml);
|
|
||||||
const notImplemented = computed(() =>
|
|
||||||
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notImplemented).map((msg) => msg.value),
|
|
||||||
);
|
|
||||||
const notComposable = computed(() =>
|
|
||||||
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notTranslatable).map((msg) => msg.value),
|
|
||||||
);
|
|
||||||
const errors = computed(() =>
|
|
||||||
conversionResult.value.messages
|
|
||||||
.filter((msg) => msg.type === MessageType.errorDuringConversion)
|
|
||||||
.map((msg) => msg.value),
|
|
||||||
);
|
|
||||||
const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value));
|
|
||||||
const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' });
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,83 +1,6 @@
|
||||||
<template>
|
|
||||||
<n-card title="Encrypt">
|
|
||||||
<n-space item-style="flex: 1 1 0">
|
|
||||||
<n-form-item label="Your text:" :show-feedback="false">
|
|
||||||
<n-input
|
|
||||||
v-model:value="cypherInput"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="The string to cypher"
|
|
||||||
:autosize="{ minRows: 4 }"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-space vertical>
|
|
||||||
<n-form-item label="Your secret key:" :show-feedback="false">
|
|
||||||
<n-input v-model:value="cypherSecret" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Encryption algorithm:" :show-feedback="false">
|
|
||||||
<n-select
|
|
||||||
v-model:value="cypherAlgo"
|
|
||||||
:options="Object.keys(algos).map((label) => ({ label, value: label }))"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
</n-space>
|
|
||||||
</n-space>
|
|
||||||
<br />
|
|
||||||
<n-form-item label="Your text encrypted:" :show-feedback="false">
|
|
||||||
<n-input
|
|
||||||
:value="cypherOutput"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="Your string hash"
|
|
||||||
:autosize="{ minRows: 2 }"
|
|
||||||
readonly
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
</n-card>
|
|
||||||
<n-card title="Decrypt">
|
|
||||||
<n-space item-style="flex: 1 1 0">
|
|
||||||
<n-form-item label="Your encrypted text:" :show-feedback="false">
|
|
||||||
<n-input
|
|
||||||
v-model:value="decryptInput"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="The string to cypher"
|
|
||||||
:autosize="{ minRows: 4 }"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-space vertical>
|
|
||||||
<n-form-item label="Your secret key:" :show-feedback="false">
|
|
||||||
<n-input v-model:value="decryptSecret" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Encryption algorithm:" :show-feedback="false">
|
|
||||||
<n-select
|
|
||||||
v-model:value="decryptAlgo"
|
|
||||||
:options="Object.keys(algos).map((label) => ({ label, value: label }))"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
</n-space>
|
|
||||||
</n-space>
|
|
||||||
<br />
|
|
||||||
<n-form-item label="Your decrypted text:" :show-feedback="false">
|
|
||||||
<n-input
|
|
||||||
:value="decryptOutput"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="Your string hash"
|
|
||||||
:autosize="{ minRows: 2 }"
|
|
||||||
readonly
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { AES, TripleDES, Rabbit, RC4, enc } from 'crypto-js';
|
import { AES, RC4, Rabbit, TripleDES, enc } from 'crypto-js';
|
||||||
|
|
||||||
const algos = { AES, TripleDES, Rabbit, RC4 };
|
const algos = { AES, TripleDES, Rabbit, RC4 };
|
||||||
|
|
||||||
|
@ -93,3 +16,84 @@ const decryptOutput = computed(() =>
|
||||||
algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8),
|
algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8),
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-card title="Encrypt">
|
||||||
|
<div flex gap-3>
|
||||||
|
<n-form-item label="Your text:" :show-feedback="false" flex-1>
|
||||||
|
<n-input
|
||||||
|
v-model:value="cypherInput"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="The string to cypher"
|
||||||
|
:autosize="{ minRows: 4 }"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<div flex flex-1 flex-col gap-2>
|
||||||
|
<c-input-text v-model:value="cypherSecret" label="Your secret key:" clearable raw-text />
|
||||||
|
|
||||||
|
<n-form-item label="Encryption algorithm:" :show-feedback="false">
|
||||||
|
<n-select
|
||||||
|
v-model:value="cypherAlgo"
|
||||||
|
:options="Object.keys(algos).map((label) => ({ label, value: label }))"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<n-form-item label="Your text encrypted:" :show-feedback="false" mt-5>
|
||||||
|
<n-input
|
||||||
|
:value="cypherOutput"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="Your string hash"
|
||||||
|
:autosize="{ minRows: 2 }"
|
||||||
|
readonly
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</c-card>
|
||||||
|
<c-card title="Decrypt">
|
||||||
|
<div flex gap-3>
|
||||||
|
<n-form-item label="Your encrypted text:" :show-feedback="false" flex-1>
|
||||||
|
<n-input
|
||||||
|
v-model:value="decryptInput"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="The string to cypher"
|
||||||
|
:autosize="{ minRows: 4 }"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<div flex flex-1 flex-col gap-2>
|
||||||
|
<c-input-text v-model:value="decryptSecret" label="Your secret key:" clearable raw-text />
|
||||||
|
|
||||||
|
<n-form-item label="Encryption algorithm:" :show-feedback="false">
|
||||||
|
<n-select
|
||||||
|
v-model:value="decryptAlgo"
|
||||||
|
:options="Object.keys(algos).map((label) => ({ label, value: label }))"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<n-form-item label="Your decrypted text:" :show-feedback="false" mt-5>
|
||||||
|
<n-input
|
||||||
|
:value="decryptOutput"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="Your string hash"
|
||||||
|
:autosize="{ minRows: 2 }"
|
||||||
|
readonly
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,60 +1,8 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-text depth="3" style="text-align: justify; width: 100%; display: inline-block">
|
|
||||||
With a concrete example, if you wash 3 plates in 5 minutes and you have 500 plates to wash, it will take you 5
|
|
||||||
hours and 10 minutes to wash them all, and if you start now, you'll end
|
|
||||||
{{ endAt }}.
|
|
||||||
</n-text>
|
|
||||||
<br />
|
|
||||||
<n-divider />
|
|
||||||
<n-space item-style="flex:1 1 0">
|
|
||||||
<div>
|
|
||||||
<n-space item-style="flex:1 1 0">
|
|
||||||
<n-form-item label="Amount of element to consume">
|
|
||||||
<n-input-number v-model:value="unitCount" :min="1" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="The consumption started at">
|
|
||||||
<n-date-picker v-model:value="startedAt" type="datetime" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-space>
|
|
||||||
|
|
||||||
<n-form-item label="Amount of unit consumed by time span" :show-feedback="false">
|
|
||||||
<n-input-number v-model:value="unitPerTimeSpan" :min="1" />
|
|
||||||
<span style="margin: 0 10px">in</span>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-number v-model:value="timeSpan" :min="1" />
|
|
||||||
<n-select
|
|
||||||
v-model:value="timeSpanUnitMultiplier"
|
|
||||||
:options="[
|
|
||||||
{ label: 'milliseconds', value: 1 },
|
|
||||||
{ label: 'seconds', value: 1000 },
|
|
||||||
{ label: 'minutes', value: 1000 * 60 },
|
|
||||||
{ label: 'hours', value: 1000 * 60 * 60 },
|
|
||||||
{ label: 'days', value: 1000 * 60 * 60 * 24 },
|
|
||||||
]"
|
|
||||||
></n-select>
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-divider />
|
|
||||||
<n-space vertical>
|
|
||||||
<n-card>
|
|
||||||
<n-statistic label="Total duration">{{ formatMsDuration(durationMs) }}</n-statistic>
|
|
||||||
</n-card>
|
|
||||||
<n-card>
|
|
||||||
<n-statistic label="It will end ">{{ endAt }}</n-statistic>
|
|
||||||
</n-card>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Duplicate issue with sub directory
|
// Duplicate issue with sub directory
|
||||||
// eslint-disable-next-line import/no-duplicates
|
|
||||||
import { addMilliseconds, formatRelative } from 'date-fns';
|
import { addMilliseconds, formatRelative } from 'date-fns';
|
||||||
// eslint-disable-next-line import/no-duplicates
|
|
||||||
import { enGB } from 'date-fns/locale';
|
import { enGB } from 'date-fns/locale';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { formatMsDuration } from './eta-calculator.service';
|
import { formatMsDuration } from './eta-calculator.service';
|
||||||
|
@ -75,6 +23,55 @@ const endAt = computed(() =>
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-text depth="3" style="text-align: justify; width: 100%; display: inline-block">
|
||||||
|
With a concrete example, if you wash 3 plates in 5 minutes and you have 500 plates to wash, it will take you 5
|
||||||
|
hours and 10 minutes to wash them all, and if you start now, you'll end
|
||||||
|
{{ endAt }}.
|
||||||
|
</n-text>
|
||||||
|
<n-divider />
|
||||||
|
<div flex gap-2>
|
||||||
|
<n-form-item label="Amount of element to consume" flex-1>
|
||||||
|
<n-input-number v-model:value="unitCount" :min="1" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="The consumption started at" flex-1>
|
||||||
|
<n-date-picker v-model:value="startedAt" type="datetime" />
|
||||||
|
</n-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-form-item label="Amount of unit consumed by time span" :show-feedback="false">
|
||||||
|
<n-input-number v-model:value="unitPerTimeSpan" :min="1" />
|
||||||
|
<span mx-3>in</span>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input-number v-model:value="timeSpan" :min="1" />
|
||||||
|
<n-select
|
||||||
|
v-model:value="timeSpanUnitMultiplier"
|
||||||
|
:options="[
|
||||||
|
{ label: 'milliseconds', value: 1 },
|
||||||
|
{ label: 'seconds', value: 1000 },
|
||||||
|
{ label: 'minutes', value: 1000 * 60 },
|
||||||
|
{ label: 'hours', value: 1000 * 60 * 60 },
|
||||||
|
{ label: 'days', value: 1000 * 60 * 60 * 24 },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
<c-card mb-2>
|
||||||
|
<n-statistic label="Total duration">
|
||||||
|
{{ formatMsDuration(durationMs) }}
|
||||||
|
</n-statistic>
|
||||||
|
</c-card>
|
||||||
|
<c-card>
|
||||||
|
<n-statistic label="It will end ">
|
||||||
|
{{ endAt }}
|
||||||
|
</n-statistic>
|
||||||
|
</c-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.n-input-number,
|
.n-input-number,
|
||||||
.n-date-picker {
|
.n-date-picker {
|
||||||
|
|
|
@ -1,9 +1,3 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<memo />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useThemeVars } from 'naive-ui';
|
import { useThemeVars } from 'naive-ui';
|
||||||
import Memo from './git-memo.md';
|
import Memo from './git-memo.md';
|
||||||
|
@ -11,6 +5,12 @@ import Memo from './git-memo.md';
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Memo />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
::v-deep(pre) {
|
::v-deep(pre) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -2,6 +2,6 @@ export function convertHexToBin(hex: string) {
|
||||||
return hex
|
return hex
|
||||||
.trim()
|
.trim()
|
||||||
.split('')
|
.split('')
|
||||||
.map((byte) => parseInt(byte, 16).toString(2).padStart(4, '0'))
|
.map(byte => parseInt(byte, 16).toString(2).padStart(4, '0'))
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,42 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { lib } from 'crypto-js';
|
||||||
|
import { MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512, enc } from 'crypto-js';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import InputCopyable from '../../components/InputCopyable.vue';
|
||||||
|
import { convertHexToBin } from './hash-text.service';
|
||||||
|
import { useQueryParam } from '@/composable/queryParams';
|
||||||
|
|
||||||
|
const algos = {
|
||||||
|
MD5,
|
||||||
|
SHA1,
|
||||||
|
SHA256,
|
||||||
|
SHA224,
|
||||||
|
SHA512,
|
||||||
|
SHA384,
|
||||||
|
SHA3,
|
||||||
|
RIPEMD160,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type AlgoNames = keyof typeof algos;
|
||||||
|
type Encoding = keyof typeof enc | 'Bin';
|
||||||
|
const algoNames = Object.keys(algos) as AlgoNames[];
|
||||||
|
const encoding = useQueryParam<Encoding>({ defaultValue: 'Hex', name: 'encoding' });
|
||||||
|
const clearText = ref('');
|
||||||
|
|
||||||
|
function formatWithEncoding(words: lib.WordArray, encoding: Encoding) {
|
||||||
|
if (encoding === 'Bin') {
|
||||||
|
return convertHexToBin(words.toString(enc.Hex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return words.toString(enc[encoding]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[algo](value), encoding.value);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-card>
|
<c-card>
|
||||||
<n-input v-model:value="clearText" type="textarea" placeholder="Your string to hash..." rows="3" />
|
<n-input v-model:value="clearText" type="textarea" placeholder="Your string to hash..." rows="3" />
|
||||||
|
|
||||||
<n-divider />
|
<n-divider />
|
||||||
|
@ -31,45 +67,12 @@
|
||||||
|
|
||||||
<div v-for="algo in algoNames" :key="algo" style="margin: 5px 0">
|
<div v-for="algo in algoNames" :key="algo" style="margin: 5px 0">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input-group-label style="flex: 0 0 120px"> {{ algo }} </n-input-group-label>
|
<n-input-group-label style="flex: 0 0 120px">
|
||||||
<input-copyable :value="hashText(algo, clearText)" readonly />
|
{{ algo }}
|
||||||
|
</n-input-group-label>
|
||||||
|
<InputCopyable :value="hashText(algo, clearText)" readonly />
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</c-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useQueryParam } from '@/composable/queryParams';
|
|
||||||
import { enc, lib, MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512 } from 'crypto-js';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import InputCopyable from '../../components/InputCopyable.vue';
|
|
||||||
import { convertHexToBin } from './hash-text.service';
|
|
||||||
|
|
||||||
const algos = {
|
|
||||||
MD5,
|
|
||||||
SHA1,
|
|
||||||
SHA256,
|
|
||||||
SHA224,
|
|
||||||
SHA512,
|
|
||||||
SHA384,
|
|
||||||
SHA3,
|
|
||||||
RIPEMD160,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type AlgoNames = keyof typeof algos;
|
|
||||||
type Encoding = keyof typeof enc | 'Bin';
|
|
||||||
const algoNames = Object.keys(algos) as AlgoNames[];
|
|
||||||
const encoding = useQueryParam<Encoding>({ defaultValue: 'Hex', name: 'encoding' });
|
|
||||||
const clearText = ref('');
|
|
||||||
|
|
||||||
function formatWithEncoding(words: lib.WordArray, encoding: Encoding) {
|
|
||||||
if (encoding === 'Bin') {
|
|
||||||
return convertHexToBin(words.toString(enc.Hex));
|
|
||||||
}
|
|
||||||
|
|
||||||
return words.toString(enc[encoding]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[algo](value), encoding.value);
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,57 +1,6 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-form-item label="Plain text to compute the hash">
|
|
||||||
<n-input v-model:value="plainText" type="textarea" placeholder="Enter the text to compute the hash..." />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Secret key">
|
|
||||||
<n-input v-model:value="secret" placeholder="Enter the secret key..." />
|
|
||||||
</n-form-item>
|
|
||||||
<n-space item-style="flex:1 1 0">
|
|
||||||
<n-form-item label="Hashing function">
|
|
||||||
<n-select
|
|
||||||
v-model:value="hashFunction"
|
|
||||||
placeholder="Select an hashing function..."
|
|
||||||
:options="Object.keys(algos).map((label) => ({ label, value: label }))"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Output encoding">
|
|
||||||
<n-select
|
|
||||||
v-model:value="encoding"
|
|
||||||
placeholder="Select the result encoding..."
|
|
||||||
:options="[
|
|
||||||
{
|
|
||||||
label: 'Binary (base 2)',
|
|
||||||
value: 'Bin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Hexadecimal (base 16)',
|
|
||||||
value: 'Hex',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Base64 (base 64)',
|
|
||||||
value: 'Base64',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Base64-url (base 64 with url safe chars)',
|
|
||||||
value: 'Base64url',
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
</n-space>
|
|
||||||
<n-form-item label="HMAC of your text">
|
|
||||||
<n-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." />
|
|
||||||
</n-form-item>
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button secondary @click="copy()">Copy HMAC</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCopy } from '@/composable/copy';
|
import type { lib } from 'crypto-js';
|
||||||
import {
|
import {
|
||||||
enc,
|
|
||||||
HmacMD5,
|
HmacMD5,
|
||||||
HmacRIPEMD160,
|
HmacRIPEMD160,
|
||||||
HmacSHA1,
|
HmacSHA1,
|
||||||
|
@ -60,10 +9,11 @@ import {
|
||||||
HmacSHA3,
|
HmacSHA3,
|
||||||
HmacSHA384,
|
HmacSHA384,
|
||||||
HmacSHA512,
|
HmacSHA512,
|
||||||
lib,
|
enc,
|
||||||
} from 'crypto-js';
|
} from 'crypto-js';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { convertHexToBin } from '../hash-text/hash-text.service';
|
import { convertHexToBin } from '../hash-text/hash-text.service';
|
||||||
|
import { useCopy } from '@/composable/copy';
|
||||||
|
|
||||||
const algos = {
|
const algos = {
|
||||||
MD5: HmacMD5,
|
MD5: HmacMD5,
|
||||||
|
@ -94,3 +44,55 @@ const hmac = computed(() =>
|
||||||
);
|
);
|
||||||
const { copy } = useCopy({ source: hmac });
|
const { copy } = useCopy({ source: hmac });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-form-item label="Plain text to compute the hash">
|
||||||
|
<n-input v-model:value="plainText" type="textarea" placeholder="Enter the text to compute the hash..." />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Secret key">
|
||||||
|
<n-input v-model:value="secret" placeholder="Enter the secret key..." />
|
||||||
|
</n-form-item>
|
||||||
|
<div flex gap-2>
|
||||||
|
<n-form-item label="Hashing function" flex-1>
|
||||||
|
<n-select
|
||||||
|
v-model:value="hashFunction"
|
||||||
|
placeholder="Select an hashing function..."
|
||||||
|
:options="Object.keys(algos).map((label) => ({ label, value: label }))"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Output encoding" flex-1>
|
||||||
|
<n-select
|
||||||
|
v-model:value="encoding"
|
||||||
|
placeholder="Select the result encoding..."
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
label: 'Binary (base 2)',
|
||||||
|
value: 'Bin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hexadecimal (base 16)',
|
||||||
|
value: 'Hex',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Base64 (base 64)',
|
||||||
|
value: 'Base64',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Base64-url (base 64 with url safe chars)',
|
||||||
|
value: 'Base64url',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</div>
|
||||||
|
<n-form-item label="HMAC of your text">
|
||||||
|
<n-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." />
|
||||||
|
</n-form-item>
|
||||||
|
<div flex justify-center>
|
||||||
|
<c-button @click="copy()">
|
||||||
|
Copy HMAC
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { escape, unescape } from 'lodash';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useCopy } from '@/composable/copy';
|
||||||
|
|
||||||
|
const escapeInput = ref('<title>IT Tool</title>');
|
||||||
|
const escapeOutput = computed(() => escape(escapeInput.value));
|
||||||
|
const { copy: copyEscaped } = useCopy({ source: escapeOutput });
|
||||||
|
|
||||||
|
const unescapeInput = ref('<title>IT Tool</title');
|
||||||
|
const unescapeOutput = computed(() => unescape(unescapeInput.value));
|
||||||
|
const { copy: copyUnescaped } = useCopy({ source: unescapeOutput });
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card title="Escape html entities">
|
<c-card title="Escape html entities">
|
||||||
<n-form-item label="Your string :">
|
<n-form-item label="Your string :">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="escapeInput"
|
v-model:value="escapeInput"
|
||||||
|
@ -19,11 +33,13 @@
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-space justify="center">
|
<div flex justify-center>
|
||||||
<n-button secondary @click="copyEscaped"> Copy </n-button>
|
<c-button @click="copyEscaped">
|
||||||
</n-space>
|
Copy
|
||||||
</n-card>
|
</c-button>
|
||||||
<n-card title="Unescape html entities">
|
</div>
|
||||||
|
</c-card>
|
||||||
|
<c-card title="Unescape html entities">
|
||||||
<n-form-item label="Your escaped string :">
|
<n-form-item label="Your escaped string :">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="unescapeInput"
|
v-model:value="unescapeInput"
|
||||||
|
@ -43,22 +59,10 @@
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-space justify="center">
|
<div flex justify-center>
|
||||||
<n-button secondary @click="copyUnescaped"> Copy </n-button>
|
<c-button @click="copyUnescaped">
|
||||||
</n-space>
|
Copy
|
||||||
</n-card>
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { escape, unescape } from 'lodash';
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useCopy } from '@/composable/copy';
|
|
||||||
|
|
||||||
const escapeInput = ref('<title>IT Tool</title>');
|
|
||||||
const escapeOutput = computed(() => escape(escapeInput.value));
|
|
||||||
const { copy: copyEscaped } = useCopy({ source: escapeOutput });
|
|
||||||
|
|
||||||
const unescapeInput = ref('<title>IT Tool</title');
|
|
||||||
const unescapeOutput = computed(() => unescape(unescapeInput.value));
|
|
||||||
const { copy: copyUnescaped } = useCopy({ source: unescapeOutput });
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,14 +1,3 @@
|
||||||
<template>
|
|
||||||
<n-card v-if="editor" class="editor">
|
|
||||||
<template #header>
|
|
||||||
<menu-bar class="editor-header" :editor="editor" />
|
|
||||||
<n-divider style="margin-top: 0" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<editor-content class="editor-content" :editor="editor" />
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { tryOnBeforeUnmount, useVModel } from '@vueuse/core';
|
import { tryOnBeforeUnmount, useVModel } from '@vueuse/core';
|
||||||
import { Editor, EditorContent } from '@tiptap/vue-3';
|
import { Editor, EditorContent } from '@tiptap/vue-3';
|
||||||
|
@ -16,9 +5,9 @@ import StarterKit from '@tiptap/starter-kit';
|
||||||
import { useThemeVars } from 'naive-ui';
|
import { useThemeVars } from 'naive-ui';
|
||||||
import MenuBar from './menu-bar.vue';
|
import MenuBar from './menu-bar.vue';
|
||||||
|
|
||||||
const themeVars = useThemeVars();
|
|
||||||
const props = defineProps<{ html: string }>();
|
const props = defineProps<{ html: string }>();
|
||||||
const emit = defineEmits(['update:html']);
|
const emit = defineEmits(['update:html']);
|
||||||
|
const themeVars = useThemeVars();
|
||||||
const html = useVModel(props, 'html', emit);
|
const html = useVModel(props, 'html', emit);
|
||||||
|
|
||||||
const editor = new Editor({
|
const editor = new Editor({
|
||||||
|
@ -33,11 +22,18 @@ tryOnBeforeUnmount(() => {
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<template>
|
||||||
::v-deep(.n-card-header) {
|
<c-card v-if="editor" important:p0>
|
||||||
padding: 0;
|
<MenuBar class="editor-header" :editor="editor" />
|
||||||
}
|
<n-divider style="margin-top: 0" />
|
||||||
|
|
||||||
|
<div px8 pb6>
|
||||||
|
<EditorContent class="editor-content" :editor="editor" />
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
::v-deep(.ProseMirror-focused) {
|
::v-deep(.ProseMirror-focused) {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,18 @@
|
||||||
<template>
|
|
||||||
<n-tooltip trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button circle quaternary :type="isActive?.() ? 'primary' : 'default'" @click="action">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="icon" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
{{ title }}
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { toRefs, type Component } from 'vue';
|
import { type Component, toRefs } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean }>();
|
const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean }>();
|
||||||
const { icon, title, action, isActive } = toRefs(props);
|
const { icon, title, action, isActive } = toRefs(props);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<template>
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<c-button circle variant="text" :type="isActive?.() ? 'primary' : 'default'" @click="action">
|
||||||
|
<n-icon :component="icon" />
|
||||||
|
</c-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{{ title }}
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,12 +1,3 @@
|
||||||
<template>
|
|
||||||
<n-space align="center" :size="0">
|
|
||||||
<template v-for="(item, index) in items">
|
|
||||||
<n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical />
|
|
||||||
<menu-bar-item v-else-if="item.type === 'button'" :key="index" v-bind="item" />
|
|
||||||
</template>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Editor } from '@tiptap/vue-3';
|
import type { Editor } from '@tiptap/vue-3';
|
||||||
import {
|
import {
|
||||||
|
@ -27,7 +18,7 @@ import {
|
||||||
Strikethrough,
|
Strikethrough,
|
||||||
TextWrap,
|
TextWrap,
|
||||||
} from '@vicons/tabler';
|
} from '@vicons/tabler';
|
||||||
import { toRefs, type Component } from 'vue';
|
import { type Component, toRefs } from 'vue';
|
||||||
import MenuBarItem from './menu-bar-item.vue';
|
import MenuBarItem from './menu-bar-item.vue';
|
||||||
|
|
||||||
const props = defineProps<{ editor: Editor }>();
|
const props = defineProps<{ editor: Editor }>();
|
||||||
|
@ -35,11 +26,11 @@ const { editor } = toRefs(props);
|
||||||
|
|
||||||
type MenuItem =
|
type MenuItem =
|
||||||
| {
|
| {
|
||||||
icon: Component;
|
icon: Component
|
||||||
title: string;
|
title: string
|
||||||
action: () => void;
|
action: () => void
|
||||||
isActive?: () => boolean;
|
isActive?: () => boolean
|
||||||
type: 'button';
|
type: 'button'
|
||||||
}
|
}
|
||||||
| { type: 'divider' };
|
| { type: 'divider' };
|
||||||
|
|
||||||
|
@ -166,4 +157,11 @@ const items: MenuItem[] = [
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<template>
|
||||||
|
<div flex items-center>
|
||||||
|
<template v-for="(item, index) in items">
|
||||||
|
<n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical />
|
||||||
|
<MenuBarItem v-else-if="item.type === 'button'" :key="index" v-bind="item" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
<template>
|
|
||||||
<editor v-model:html="html" />
|
|
||||||
<textarea-copyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
|
||||||
import { format } from 'prettier';
|
import { format } from 'prettier';
|
||||||
import htmlParser from 'prettier/parser-html';
|
import htmlParser from 'prettier/parser-html';
|
||||||
import { useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import Editor from './editor/editor.vue';
|
import Editor from './editor/editor.vue';
|
||||||
|
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||||
|
|
||||||
const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>');
|
const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<template>
|
||||||
|
<Editor v-model:html="html" />
|
||||||
|
<TextareaCopyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" />
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
export const codesByCategories: {
|
export const codesByCategories: {
|
||||||
category: string;
|
category: string
|
||||||
codes: {
|
codes: {
|
||||||
code: number;
|
code: number
|
||||||
name: string;
|
name: string
|
||||||
description: string;
|
description: string
|
||||||
type: 'HTTP' | 'WebDav';
|
type: 'HTTP' | 'WebDav'
|
||||||
}[];
|
}[]
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
category: '1xx informational response',
|
category: '1xx informational response',
|
||||||
|
@ -286,7 +286,7 @@ export const codesByCategories: {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 418,
|
code: 418,
|
||||||
name: "I'm a teapot",
|
name: 'I\'m a teapot',
|
||||||
description: 'The server refuses the attempt to brew coffee with a teapot.',
|
description: 'The server refuses the attempt to brew coffee with a teapot.',
|
||||||
type: 'HTTP',
|
type: 'HTTP',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
test.describe('Tool - Http status codes', () => {
|
test.describe('Tool - Http status codes', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|
|
@ -1,3 +1,27 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SearchRound } from '@vicons/material';
|
||||||
|
import { codesByCategories } from './http-status-codes.constants';
|
||||||
|
import { useFuzzySearch } from '@/composable/fuzzySearch';
|
||||||
|
|
||||||
|
const search = ref('');
|
||||||
|
|
||||||
|
const { searchResult } = useFuzzySearch({
|
||||||
|
search,
|
||||||
|
data: codesByCategories.flatMap(({ codes, category }) => codes.map(code => ({ ...code, category }))),
|
||||||
|
options: {
|
||||||
|
keys: [{ name: 'code', weight: 3 }, { name: 'name', weight: 2 }, 'description', 'category'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const codesByCategoryFiltered = computed(() => {
|
||||||
|
if (!search.value) {
|
||||||
|
return codesByCategories;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ category: 'Search results', codes: searchResult.value }];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-form-item :show-label="false">
|
<n-form-item :show-label="false">
|
||||||
|
@ -20,41 +44,14 @@
|
||||||
<div v-for="{ codes, category } of codesByCategoryFiltered" :key="category" mb-8>
|
<div v-for="{ codes, category } of codesByCategoryFiltered" :key="category" mb-8>
|
||||||
<n-h2> {{ category }} </n-h2>
|
<n-h2> {{ category }} </n-h2>
|
||||||
|
|
||||||
<n-space vertical :size="20">
|
<c-card v-for="{ code, description, name, type } of codes" :key="code" mb-2>
|
||||||
<n-card v-for="{ code, description, name, type } of codes" :key="code">
|
<n-text strong block text-lg>
|
||||||
<n-space align="center">
|
{{ code }} {{ name }}
|
||||||
<n-text strong text-lg> {{ code }} {{ name }} </n-text>
|
</n-text>
|
||||||
</n-space>
|
<n-text block depth="3">
|
||||||
<n-text depth="3">{{ description }} {{ type !== 'HTTP' ? `For ${type}.` : '' }}</n-text>
|
{{ description }} {{ type !== 'HTTP' ? `For ${type}.` : '' }}
|
||||||
</n-card>
|
</n-text>
|
||||||
</n-space>
|
</c-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useFuzzySearch } from '@/composable/fuzzySearch';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { SearchRound } from '@vicons/material';
|
|
||||||
import { codesByCategories } from './http-status-codes.constants';
|
|
||||||
|
|
||||||
const search = ref('');
|
|
||||||
|
|
||||||
const { searchResult } = useFuzzySearch({
|
|
||||||
search,
|
|
||||||
data: codesByCategories.flatMap(({ codes, category }) => codes.map((code) => ({ ...code, category }))),
|
|
||||||
options: {
|
|
||||||
keys: [{ name: 'code', weight: 3 }, { name: 'name', weight: 2 }, 'description', 'category'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const codesByCategoryFiltered = computed(() => {
|
|
||||||
if (!search.value) {
|
|
||||||
return codesByCategories;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [{ category: 'Search results', codes: searchResult.value }];
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { tool as base64FileConverter } from './base64-file-converter';
|
import { tool as base64FileConverter } from './base64-file-converter';
|
||||||
import { tool as base64StringConverter } from './base64-string-converter';
|
import { tool as base64StringConverter } from './base64-string-converter';
|
||||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
||||||
|
import { tool as cameraRecorder } from './camera-recorder';
|
||||||
|
import { tool as listConverter } from './list-converter';
|
||||||
|
import { tool as phoneParserAndFormatter } from './phone-parser-and-formatter';
|
||||||
|
import { tool as jsonDiff } from './json-diff';
|
||||||
|
import { tool as ipv4RangeExpander } from './ipv4-range-expander';
|
||||||
import { tool as httpStatusCodes } from './http-status-codes';
|
import { tool as httpStatusCodes } from './http-status-codes';
|
||||||
import { tool as yamlToJson } from './yaml-to-json-converter';
|
import { tool as yamlToJson } from './yaml-to-json-converter';
|
||||||
import { tool as jsonToYaml } from './json-to-yaml-converter';
|
import { tool as jsonToYaml } from './json-to-yaml-converter';
|
||||||
|
@ -71,6 +76,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||||
textToNatoAlphabet,
|
textToNatoAlphabet,
|
||||||
yamlToJson,
|
yamlToJson,
|
||||||
jsonToYaml,
|
jsonToYaml,
|
||||||
|
listConverter,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -90,11 +96,12 @@ export const toolsByCategory: ToolCategory[] = [
|
||||||
htmlWysiwygEditor,
|
htmlWysiwygEditor,
|
||||||
userAgentParser,
|
userAgentParser,
|
||||||
httpStatusCodes,
|
httpStatusCodes,
|
||||||
|
jsonDiff,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Images',
|
name: 'Images and videos',
|
||||||
components: [qrCodeGenerator, svgPlaceholderGenerator],
|
components: [qrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Development',
|
name: 'Development',
|
||||||
|
@ -111,7 +118,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Network',
|
name: 'Network',
|
||||||
components: [ipv4SubnetCalculator, ipv4AddressConverter, macAddressLookup, ipv6UlaGenerator],
|
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, ipv6UlaGenerator],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Math',
|
name: 'Math',
|
||||||
|
@ -125,9 +132,13 @@ export const toolsByCategory: ToolCategory[] = [
|
||||||
name: 'Text',
|
name: 'Text',
|
||||||
components: [loremIpsumGenerator, textStatistics],
|
components: [loremIpsumGenerator, textStatistics],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Data',
|
||||||
|
components: [phoneParserAndFormatter],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const tools = toolsByCategory.flatMap(({ components }) => components);
|
export const tools = toolsByCategory.flatMap(({ components }) => components);
|
||||||
export const toolsWithCategory = toolsByCategory.flatMap(({ components, name }) =>
|
export const toolsWithCategory = toolsByCategory.flatMap(({ components, name }) =>
|
||||||
components.map((tool) => ({ category: name, ...tool })),
|
components.map(tool => ({ category: name, ...tool })),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { expect, describe, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { convertBase } from './integer-base-converter.model';
|
import { convertBase } from './integer-base-converter.model';
|
||||||
|
|
||||||
describe('integer-base-converter', () => {
|
describe('integer-base-converter', () => {
|
||||||
|
|
|
@ -7,9 +7,9 @@ export function convertBase({ value, fromBase, toBase }: { value: string; fromBa
|
||||||
.reverse()
|
.reverse()
|
||||||
.reduce((carry: number, digit: string, index: number) => {
|
.reduce((carry: number, digit: string, index: number) => {
|
||||||
if (!fromRange.includes(digit)) {
|
if (!fromRange.includes(digit)) {
|
||||||
throw new Error('Invalid digit "' + digit + '" for base ' + fromBase + '.');
|
throw new Error(`Invalid digit "${digit}" for base ${fromBase}.`);
|
||||||
}
|
}
|
||||||
return (carry += fromRange.indexOf(digit) * Math.pow(fromBase, index));
|
return (carry += fromRange.indexOf(digit) * fromBase ** index);
|
||||||
}, 0);
|
}, 0);
|
||||||
let newValue = '';
|
let newValue = '';
|
||||||
while (decValue > 0) {
|
while (decValue > 0) {
|
||||||
|
|
|
@ -1,93 +1,20 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-card>
|
|
||||||
<div v-if="styleStore.isSmallScreen">
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
|
|
||||||
<n-input v-model:value="input" w-full :status="error ? 'error' : undefined" />
|
|
||||||
</n-input-group>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label>
|
|
||||||
<n-input-number v-model:value="inputBase" max="64" min="2" w-full />
|
|
||||||
</n-input-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-input-group v-else>
|
|
||||||
<n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
|
|
||||||
<n-input v-model:value="input" :status="error ? 'error' : undefined" />
|
|
||||||
<n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label>
|
|
||||||
<n-input-number v-model:value="inputBase" max="64" min="2" />
|
|
||||||
</n-input-group>
|
|
||||||
|
|
||||||
<n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert>
|
|
||||||
<n-divider />
|
|
||||||
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label style="flex: 0 0 170px"> Binary (2): </n-input-group-label>
|
|
||||||
<input-copyable
|
|
||||||
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
|
|
||||||
readonly
|
|
||||||
placeholder="Binary version will be here..."
|
|
||||||
/>
|
|
||||||
</n-input-group>
|
|
||||||
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label style="flex: 0 0 170px"> Octal (8): </n-input-group-label>
|
|
||||||
<input-copyable
|
|
||||||
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
|
|
||||||
readonly
|
|
||||||
placeholder="Octal version will be here..."
|
|
||||||
/>
|
|
||||||
</n-input-group>
|
|
||||||
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label style="flex: 0 0 170px"> Decimal (10): </n-input-group-label>
|
|
||||||
<input-copyable
|
|
||||||
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
|
|
||||||
readonly
|
|
||||||
placeholder="Decimal version will be here..."
|
|
||||||
/>
|
|
||||||
</n-input-group>
|
|
||||||
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label style="flex: 0 0 170px"> Hexadecimal (16): </n-input-group-label>
|
|
||||||
<input-copyable
|
|
||||||
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })"
|
|
||||||
readonly
|
|
||||||
placeholder="Decimal version will be here..."
|
|
||||||
/>
|
|
||||||
</n-input-group>
|
|
||||||
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label style="flex: 0 0 170px"> Base64 (64): </n-input-group-label>
|
|
||||||
<input-copyable
|
|
||||||
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })"
|
|
||||||
readonly
|
|
||||||
placeholder="Base64 version will be here..."
|
|
||||||
/>
|
|
||||||
</n-input-group>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label style="flex: 0 0 85px"> Custom: </n-input-group-label>
|
|
||||||
<n-input-number v-model:value="outputBase" style="flex: 0 0 86px" max="64" min="2" />
|
|
||||||
<input-copyable
|
|
||||||
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })"
|
|
||||||
readonly
|
|
||||||
:placeholder="`Base ${outputBase} will be here...`"
|
|
||||||
/>
|
|
||||||
</n-input-group>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import InputCopyable from '../../components/InputCopyable.vue';
|
||||||
|
import { convertBase } from './integer-base-converter.model';
|
||||||
import { useStyleStore } from '@/stores/style.store';
|
import { useStyleStore } from '@/stores/style.store';
|
||||||
import { getErrorMessageIfThrows } from '@/utils/error';
|
import { getErrorMessageIfThrows } from '@/utils/error';
|
||||||
import { convertBase } from './integer-base-converter.model';
|
|
||||||
import InputCopyable from '../../components/InputCopyable.vue';
|
|
||||||
|
|
||||||
const styleStore = useStyleStore();
|
const styleStore = useStyleStore();
|
||||||
|
|
||||||
|
const inputProps = {
|
||||||
|
'labelPosition': 'left',
|
||||||
|
'labelWidth': '170px',
|
||||||
|
'labelAlign': 'right',
|
||||||
|
'readonly': true,
|
||||||
|
'mb-2': '',
|
||||||
|
} as const;
|
||||||
|
|
||||||
const input = ref('42');
|
const input = ref('42');
|
||||||
const inputBase = ref(10);
|
const inputBase = ref(10);
|
||||||
const outputBase = ref(42);
|
const outputBase = ref(42);
|
||||||
|
@ -95,7 +22,8 @@ const outputBase = ref(42);
|
||||||
function errorlessConvert(...args: Parameters<typeof convertBase>) {
|
function errorlessConvert(...args: Parameters<typeof convertBase>) {
|
||||||
try {
|
try {
|
||||||
return convertBase(...args);
|
return convertBase(...args);
|
||||||
} catch (err) {
|
}
|
||||||
|
catch (err) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,6 +35,92 @@ const error = computed(() =>
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<c-card>
|
||||||
|
<div v-if="styleStore.isSmallScreen">
|
||||||
|
<n-input-group>
|
||||||
|
<n-input-group-label style="flex: 0 0 120px">
|
||||||
|
Input number:
|
||||||
|
</n-input-group-label>
|
||||||
|
<n-input v-model:value="input" w-full :status="error ? 'error' : undefined" />
|
||||||
|
</n-input-group>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input-group-label style="flex: 0 0 120px">
|
||||||
|
Input base:
|
||||||
|
</n-input-group-label>
|
||||||
|
<n-input-number v-model:value="inputBase" max="64" min="2" w-full />
|
||||||
|
</n-input-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-input-group v-else>
|
||||||
|
<n-input-group-label style="flex: 0 0 120px">
|
||||||
|
Input number:
|
||||||
|
</n-input-group-label>
|
||||||
|
<n-input v-model:value="input" :status="error ? 'error' : undefined" />
|
||||||
|
<n-input-group-label style="flex: 0 0 120px">
|
||||||
|
Input base:
|
||||||
|
</n-input-group-label>
|
||||||
|
<n-input-number v-model:value="inputBase" max="64" min="2" />
|
||||||
|
</n-input-group>
|
||||||
|
|
||||||
|
<n-alert v-if="error" style="margin-top: 25px" type="error">
|
||||||
|
{{ error }}
|
||||||
|
</n-alert>
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<InputCopyable
|
||||||
|
label="Binary (2)"
|
||||||
|
v-bind="inputProps"
|
||||||
|
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
|
||||||
|
placeholder="Binary version will be here..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputCopyable
|
||||||
|
label="Octal (8)"
|
||||||
|
v-bind="inputProps"
|
||||||
|
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
|
||||||
|
placeholder="Octal version will be here..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputCopyable
|
||||||
|
label="Decimal (10)"
|
||||||
|
v-bind="inputProps"
|
||||||
|
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
|
||||||
|
placeholder="Decimal version will be here..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputCopyable
|
||||||
|
label="Hexadecimal (16)"
|
||||||
|
v-bind="inputProps"
|
||||||
|
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })"
|
||||||
|
placeholder="Hexadecimal version will be here..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputCopyable
|
||||||
|
label="Base64 (64)"
|
||||||
|
v-bind="inputProps"
|
||||||
|
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })"
|
||||||
|
placeholder="Base64 version will be here..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div flex items-baseline>
|
||||||
|
<n-input-group style="width: 160px; margin-right: 10px">
|
||||||
|
<n-input-group-label> Custom: </n-input-group-label>
|
||||||
|
<n-input-number v-model:value="outputBase" max="64" min="2" />
|
||||||
|
</n-input-group>
|
||||||
|
|
||||||
|
<InputCopyable
|
||||||
|
flex-1
|
||||||
|
v-bind="inputProps"
|
||||||
|
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })"
|
||||||
|
:placeholder="`Base ${outputBase} will be here...`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.n-input-group:not(:first-child) {
|
.n-input-group:not(:first-child) {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect, describe, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { isValidIpv4, ipv4ToInt } from './ipv4-address-converter.service';
|
import { ipv4ToInt, isValidIpv4 } from './ipv4-address-converter.service';
|
||||||
|
|
||||||
describe('ipv4-address-converter', () => {
|
describe('ipv4-address-converter', () => {
|
||||||
describe('ipv4ToInt', () => {
|
describe('ipv4ToInt', () => {
|
||||||
|
|
|
@ -10,7 +10,7 @@ function ipv4ToInt({ ip }: { ip: string }) {
|
||||||
return ip
|
return ip
|
||||||
.trim()
|
.trim()
|
||||||
.split('.')
|
.split('.')
|
||||||
.reduce((acc, part, index) => acc + Number(part) * Math.pow(256, 3 - index), 0);
|
.reduce((acc, part, index) => acc + Number(part) * 256 ** (3 - index), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: string; prefix?: string }) {
|
function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: string; prefix?: string }) {
|
||||||
|
@ -19,13 +19,13 @@ function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: str
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
prefix +
|
prefix
|
||||||
_.chain(ip)
|
+ _.chain(ip)
|
||||||
.trim()
|
.trim()
|
||||||
.split('.')
|
.split('.')
|
||||||
.map((part) => parseInt(part).toString(16).padStart(2, '0'))
|
.map(part => parseInt(part).toString(16).padStart(2, '0'))
|
||||||
.chunk(2)
|
.chunk(2)
|
||||||
.map((blocks) => blocks.join(''))
|
.map(blocks => blocks.join(''))
|
||||||
.join(':')
|
.join(':')
|
||||||
.value()
|
.value()
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,30 +1,7 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-form-item label="An ipv4 address:" v-bind="validationAttrs">
|
|
||||||
<n-input v-model:value="rawIpAddress" placeholder="An ipv4 address..." />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-divider style="margin-top: 0" mt-0 />
|
|
||||||
|
|
||||||
<n-form-item
|
|
||||||
v-for="{ label, value } of convertedSections"
|
|
||||||
:key="label"
|
|
||||||
:label="label"
|
|
||||||
label-placement="left"
|
|
||||||
label-width="100"
|
|
||||||
>
|
|
||||||
<input-copyable
|
|
||||||
:value="validationAttrs.validationStatus === 'error' ? '' : value"
|
|
||||||
placeholder="Set a correct ipv4 address"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useValidation } from '@/composable/validation';
|
|
||||||
import { convertBase } from '../integer-base-converter/integer-base-converter.model';
|
import { convertBase } from '../integer-base-converter/integer-base-converter.model';
|
||||||
import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from './ipv4-address-converter.service';
|
import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from './ipv4-address-converter.service';
|
||||||
|
import { useValidation } from '@/composable/validation';
|
||||||
|
|
||||||
const rawIpAddress = useStorage('ipv4-converter:ip', '192.168.1.1');
|
const rawIpAddress = useStorage('ipv4-converter:ip', '192.168.1.1');
|
||||||
|
|
||||||
|
@ -57,8 +34,26 @@ const convertedSections = computed(() => {
|
||||||
|
|
||||||
const { attrs: validationAttrs } = useValidation({
|
const { attrs: validationAttrs } = useValidation({
|
||||||
source: rawIpAddress,
|
source: rawIpAddress,
|
||||||
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }],
|
rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }],
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<template>
|
||||||
|
<div>
|
||||||
|
<c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." />
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<input-copyable
|
||||||
|
v-for="{ label, value } of convertedSections"
|
||||||
|
:key="label"
|
||||||
|
:label="label"
|
||||||
|
label-position="left"
|
||||||
|
label-width="100px"
|
||||||
|
label-align="right"
|
||||||
|
mb-2
|
||||||
|
:value="validationAttrs.validationStatus === 'error' ? '' : value"
|
||||||
|
placeholder="Set a correct ipv4 address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
13
src/tools/ipv4-range-expander/index.ts
Normal file
13
src/tools/ipv4-range-expander/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { UnfoldMoreOutlined } from '@vicons/material';
|
||||||
|
import { defineTool } from '../tool';
|
||||||
|
|
||||||
|
export const tool = defineTool({
|
||||||
|
name: 'IPv4 range expander',
|
||||||
|
path: '/ipv4-range-expander',
|
||||||
|
description:
|
||||||
|
'Given a start and an end IPv4 address this tool calculates a valid IPv4 network with its CIDR notation.',
|
||||||
|
keywords: ['ipv4', 'range', 'expander', 'subnet', 'creator', 'cidr'],
|
||||||
|
component: () => import('./ipv4-range-expander.vue'),
|
||||||
|
icon: UnfoldMoreOutlined,
|
||||||
|
createdAt: new Date('2023-04-19'),
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Tool - IPv4 range expander', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/ipv4-range-expander');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Has correct title', async ({ page }) => {
|
||||||
|
await expect(page).toHaveTitle('IPv4 range expander - IT Tools');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Calculates correct for valid input', async ({ page }) => {
|
||||||
|
await page.getByPlaceholder('Start IPv4 address...').fill('192.168.1.1');
|
||||||
|
await page.getByPlaceholder('End IPv4 address...').fill('192.168.7.255');
|
||||||
|
|
||||||
|
expect(await page.getByTestId('start-address.old').textContent()).toEqual('192.168.1.1');
|
||||||
|
expect(await page.getByTestId('start-address.new').textContent()).toEqual('192.168.0.0');
|
||||||
|
expect(await page.getByTestId('end-address.old').textContent()).toEqual('192.168.7.255');
|
||||||
|
expect(await page.getByTestId('end-address.new').textContent()).toEqual('192.168.7.255');
|
||||||
|
expect(await page.getByTestId('addresses-in-range.old').textContent()).toEqual('1,791');
|
||||||
|
expect(await page.getByTestId('addresses-in-range.new').textContent()).toEqual('2,048');
|
||||||
|
expect(await page.getByTestId('cidr.old').textContent()).toEqual('');
|
||||||
|
expect(await page.getByTestId('cidr.new').textContent()).toEqual('192.168.0.0/21');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Calculates correct for valid input, where first octet is lower than 128', async ({ page }) => {
|
||||||
|
await page.getByPlaceholder('Start IPv4 address...').fill('10.0.0.1');
|
||||||
|
await page.getByPlaceholder('End IPv4 address...').fill('10.0.0.17');
|
||||||
|
|
||||||
|
expect(await page.getByTestId('start-address.old').textContent()).toEqual('10.0.0.1');
|
||||||
|
expect(await page.getByTestId('start-address.new').textContent()).toEqual('10.0.0.0');
|
||||||
|
expect(await page.getByTestId('end-address.old').textContent()).toEqual('10.0.0.17');
|
||||||
|
expect(await page.getByTestId('end-address.new').textContent()).toEqual('10.0.0.31');
|
||||||
|
expect(await page.getByTestId('addresses-in-range.old').textContent()).toEqual('17');
|
||||||
|
expect(await page.getByTestId('addresses-in-range.new').textContent()).toEqual('32');
|
||||||
|
expect(await page.getByTestId('cidr.old').textContent()).toEqual('');
|
||||||
|
expect(await page.getByTestId('cidr.new').textContent()).toEqual('10.0.0.0/27');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Hides result for invalid input', async ({ page }) => {
|
||||||
|
await page.getByPlaceholder('Start IPv4 address...').fill('192.168.1.1');
|
||||||
|
await page.getByPlaceholder('End IPv4 address...').fill('192.168.0.255');
|
||||||
|
|
||||||
|
await expect(page.getByTestId('result')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { calculateCidr } from './ipv4-range-expander.service';
|
||||||
|
|
||||||
|
describe('ipv4RangeExpander', () => {
|
||||||
|
describe('when there are two valid ipv4 addresses given', () => {
|
||||||
|
it('should calculate valid cidr for given addresses', () => {
|
||||||
|
const result = calculateCidr({ startIp: '192.168.1.1', endIp: '192.168.7.255' });
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result?.oldSize).toEqual(1791);
|
||||||
|
expect(result?.newSize).toEqual(2048);
|
||||||
|
expect(result?.newStart).toEqual('192.168.0.0');
|
||||||
|
expect(result?.newEnd).toEqual('192.168.7.255');
|
||||||
|
expect(result?.newCidr).toEqual('192.168.0.0/21');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate valid cidr for given addresses, where first octet is lower than 128', () => {
|
||||||
|
const result = calculateCidr({ startIp: '10.0.0.1', endIp: '10.0.0.17' });
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result?.oldSize).toEqual(17);
|
||||||
|
expect(result?.newSize).toEqual(32);
|
||||||
|
expect(result?.newStart).toEqual('10.0.0.0');
|
||||||
|
expect(result?.newEnd).toEqual('10.0.0.31');
|
||||||
|
expect(result?.newCidr).toEqual('10.0.0.0/27');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty result for invalid input', () => {
|
||||||
|
expect(calculateCidr({ startIp: '192.168.7.1', endIp: '192.168.6.255' })).not.toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
68
src/tools/ipv4-range-expander/ipv4-range-expander.service.ts
Normal file
68
src/tools/ipv4-range-expander/ipv4-range-expander.service.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { convertBase } from '../integer-base-converter/integer-base-converter.model';
|
||||||
|
import { ipv4ToInt } from '../ipv4-address-converter/ipv4-address-converter.service';
|
||||||
|
import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types';
|
||||||
|
|
||||||
|
export { calculateCidr };
|
||||||
|
|
||||||
|
function bits2ip(ipInt: number) {
|
||||||
|
return `${ipInt >>> 24}.${(ipInt >> 16) & 255}.${(ipInt >> 8) & 255}.${ipInt & 255}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRangesize(start: string, end: string) {
|
||||||
|
if (start == null || end == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1 + parseInt(end, 2) - parseInt(start, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCidr(start: string, end: string) {
|
||||||
|
if (start == null || end == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = getRangesize(start, end);
|
||||||
|
if (range < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mask = 32;
|
||||||
|
for (let i = 0; i < 32; i++) {
|
||||||
|
if (start[i] !== end[i]) {
|
||||||
|
mask = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStart = start.substring(0, mask) + '0'.repeat(32 - mask);
|
||||||
|
const newEnd = end.substring(0, mask) + '1'.repeat(32 - mask);
|
||||||
|
|
||||||
|
return { start: newStart, end: newEnd, mask };
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) {
|
||||||
|
const start = convertBase({
|
||||||
|
value: ipv4ToInt({ ip: startIp }).toString(),
|
||||||
|
fromBase: 10,
|
||||||
|
toBase: 2,
|
||||||
|
}).padStart(32, '0');
|
||||||
|
const end = convertBase({
|
||||||
|
value: ipv4ToInt({ ip: endIp }).toString(),
|
||||||
|
fromBase: 10,
|
||||||
|
toBase: 2,
|
||||||
|
}).padStart(32, '0');
|
||||||
|
|
||||||
|
const cidr = getCidr(start, end);
|
||||||
|
if (cidr != null) {
|
||||||
|
const result: Ipv4RangeExpanderResult = {};
|
||||||
|
result.newEnd = bits2ip(parseInt(cidr.end, 2));
|
||||||
|
result.newStart = bits2ip(parseInt(cidr.start, 2));
|
||||||
|
result.newCidr = `${result.newStart}/${cidr.mask}`;
|
||||||
|
result.newSize = getRangesize(cidr.start, cidr.end);
|
||||||
|
|
||||||
|
result.oldSize = getRangesize(start, end);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface Ipv4RangeExpanderResult {
|
||||||
|
oldSize?: number
|
||||||
|
newStart?: string
|
||||||
|
newEnd?: string
|
||||||
|
newCidr?: string
|
||||||
|
newSize?: number
|
||||||
|
}
|
119
src/tools/ipv4-range-expander/ipv4-range-expander.vue
Normal file
119
src/tools/ipv4-range-expander/ipv4-range-expander.vue
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Exchange } from '@vicons/tabler';
|
||||||
|
import { isValidIpv4 } from '../ipv4-address-converter/ipv4-address-converter.service';
|
||||||
|
import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types';
|
||||||
|
import { calculateCidr } from './ipv4-range-expander.service';
|
||||||
|
import ResultRow from './result-row.vue';
|
||||||
|
import { useValidation } from '@/composable/validation';
|
||||||
|
|
||||||
|
const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1');
|
||||||
|
const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255');
|
||||||
|
|
||||||
|
const result = computed(() => calculateCidr({ startIp: rawStartAddress.value, endIp: rawEndAddress.value }));
|
||||||
|
|
||||||
|
const calculatedValues: {
|
||||||
|
label: string
|
||||||
|
getOldValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined
|
||||||
|
getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
label: 'Start address',
|
||||||
|
getOldValue: () => rawStartAddress.value,
|
||||||
|
getNewValue: result => result?.newStart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'End address',
|
||||||
|
getOldValue: () => rawEndAddress.value,
|
||||||
|
getNewValue: result => result?.newEnd,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Addresses in range',
|
||||||
|
getOldValue: result => result?.oldSize?.toLocaleString(),
|
||||||
|
getNewValue: result => result?.newSize?.toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'CIDR',
|
||||||
|
getOldValue: () => '',
|
||||||
|
getNewValue: result => result?.newCidr,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const startIpValidation = useValidation({
|
||||||
|
source: rawStartAddress,
|
||||||
|
rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }],
|
||||||
|
});
|
||||||
|
const endIpValidation = useValidation({
|
||||||
|
source: rawEndAddress,
|
||||||
|
rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined);
|
||||||
|
|
||||||
|
function onSwitchStartEndClicked() {
|
||||||
|
const tmpStart = rawStartAddress.value;
|
||||||
|
rawStartAddress.value = rawEndAddress.value;
|
||||||
|
rawEndAddress.value = tmpStart;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div mb-4 flex gap-4>
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="rawStartAddress"
|
||||||
|
label="Start address"
|
||||||
|
placeholder="Start IPv4 address..."
|
||||||
|
:validation="startIpValidation"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="rawEndAddress"
|
||||||
|
label="End address"
|
||||||
|
placeholder="End IPv4 address..."
|
||||||
|
:validation="endIpValidation"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-table v-if="showResult" data-test-id="result">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">
|
||||||
|
|
||||||
|
</th>
|
||||||
|
<th scope="col">
|
||||||
|
old value
|
||||||
|
</th>
|
||||||
|
<th scope="col">
|
||||||
|
new value
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<ResultRow
|
||||||
|
v-for="{ label, getOldValue, getNewValue } in calculatedValues"
|
||||||
|
:key="label"
|
||||||
|
:label="label"
|
||||||
|
:old-value="getOldValue(result)"
|
||||||
|
:new-value="getNewValue(result)"
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</n-table>
|
||||||
|
<n-alert
|
||||||
|
v-else-if="startIpValidation.isValid && endIpValidation.isValid"
|
||||||
|
title="Invalid combination of start and end IPv4 address"
|
||||||
|
type="error"
|
||||||
|
>
|
||||||
|
<n-text depth="3" my-3 block>
|
||||||
|
The end IPv4 address is lower than the start IPv4 address. This is not valid and no result could be calculated.
|
||||||
|
In the most cases the solution to solve this problem is to change start and end address.
|
||||||
|
</n-text>
|
||||||
|
|
||||||
|
<c-button @click="onSwitchStartEndClicked">
|
||||||
|
<n-icon mr-2 :component="Exchange" depth="3" size="22" />
|
||||||
|
Switch start and end IPv4 address
|
||||||
|
</c-button>
|
||||||
|
</n-alert>
|
||||||
|
</div>
|
||||||
|
</template>
|
29
src/tools/ipv4-range-expander/result-row.vue
Normal file
29
src/tools/ipv4-range-expander/result-row.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import _ from 'lodash';
|
||||||
|
import SpanCopyable from '@/components/SpanCopyable.vue';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ label: string; oldValue?: string; newValue?: string }>(), {
|
||||||
|
label: '',
|
||||||
|
oldValue: '',
|
||||||
|
newValue: '',
|
||||||
|
});
|
||||||
|
const { label, oldValue, newValue } = toRefs(props);
|
||||||
|
|
||||||
|
const testId = computed(() => _.kebabCase(label.value));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<n-text strong>
|
||||||
|
{{ label }}
|
||||||
|
</n-text>
|
||||||
|
</td>
|
||||||
|
<td :data-test-id="`${testId}.old`">
|
||||||
|
<SpanCopyable :value="oldValue" class="monospace" />
|
||||||
|
</td>
|
||||||
|
<td :data-test-id="`${testId}.new`">
|
||||||
|
<SpanCopyable :value="newValue" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
|
@ -1,48 +1,12 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-form-item label="An IPv4 address with or without mask" v-bind="validationAttrs">
|
|
||||||
<n-input v-model:value="ip" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<div v-if="networkInfo">
|
|
||||||
<n-table>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="{ getValue, label, undefinedFallback } in sections" :key="label">
|
|
||||||
<td>
|
|
||||||
<n-text strong>{{ label }}</n-text>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<copyable-ip-like v-if="getValue(networkInfo)" :ip="getValue(networkInfo)"></copyable-ip-like>
|
|
||||||
<n-text v-else depth="3">{{ undefinedFallback }}</n-text>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</n-table>
|
|
||||||
|
|
||||||
<n-space style="margin-top: 14px" justify="space-between">
|
|
||||||
<n-button tertiary @click="switchToBlock({ count: -1 })">
|
|
||||||
<n-icon :component="ArrowLeft" />
|
|
||||||
Previous block
|
|
||||||
</n-button>
|
|
||||||
<n-button tertiary @click="switchToBlock({ count: 1 })">
|
|
||||||
Next block
|
|
||||||
<n-icon :component="ArrowRight" />
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Netmask } from 'netmask';
|
import { Netmask } from 'netmask';
|
||||||
import { withDefaultOnError } from '@/utils/defaults';
|
|
||||||
import { useValidation } from '@/composable/validation';
|
|
||||||
import { isNotThrowing } from '@/utils/boolean';
|
|
||||||
import { useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import { ArrowLeft, ArrowRight } from '@vicons/tabler';
|
import { ArrowLeft, ArrowRight } from '@vicons/tabler';
|
||||||
import { getIPClass } from './ipv4-subnet-calculator.models';
|
import { getIPClass } from './ipv4-subnet-calculator.models';
|
||||||
import CopyableIpLike from './copyable-ip-like.vue';
|
import { withDefaultOnError } from '@/utils/defaults';
|
||||||
|
import { isNotThrowing } from '@/utils/boolean';
|
||||||
|
import SpanCopyable from '@/components/SpanCopyable.vue';
|
||||||
|
|
||||||
const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24');
|
const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24');
|
||||||
|
|
||||||
|
@ -50,24 +14,21 @@ const getNetworkInfo = (address: string) => new Netmask(address.trim());
|
||||||
|
|
||||||
const networkInfo = computed(() => withDefaultOnError(() => getNetworkInfo(ip.value), undefined));
|
const networkInfo = computed(() => withDefaultOnError(() => getNetworkInfo(ip.value), undefined));
|
||||||
|
|
||||||
const { attrs: validationAttrs } = useValidation({
|
const ipValidationRules = [
|
||||||
source: ip,
|
|
||||||
rules: [
|
|
||||||
{
|
{
|
||||||
message: 'We cannot parse this address, check the format',
|
message: 'We cannot parse this address, check the format',
|
||||||
validator: (value) => isNotThrowing(() => getNetworkInfo(value.trim())),
|
validator: (value: string) => isNotThrowing(() => getNetworkInfo(value.trim())),
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
});
|
|
||||||
|
|
||||||
const sections: {
|
const sections: {
|
||||||
label: string;
|
label: string
|
||||||
getValue: (blocks: Netmask) => string | undefined;
|
getValue: (blocks: Netmask) => string | undefined
|
||||||
undefinedFallback?: string;
|
undefinedFallback?: string
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
label: 'Netmask',
|
label: 'Netmask',
|
||||||
getValue: (block) => block.toString(),
|
getValue: block => block.toString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Network address',
|
label: 'Network address',
|
||||||
|
@ -122,4 +83,45 @@ function switchToBlock({ count = 1 }: { count?: number }) {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<template>
|
||||||
|
<div>
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="ip"
|
||||||
|
label="An IPv4 address with or without mask"
|
||||||
|
placeholder="The ipv4 address..."
|
||||||
|
:validation-rules="ipValidationRules"
|
||||||
|
mb-4
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="networkInfo">
|
||||||
|
<n-table>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="{ getValue, label, undefinedFallback } in sections" :key="label">
|
||||||
|
<td>
|
||||||
|
<n-text strong>
|
||||||
|
{{ label }}
|
||||||
|
</n-text>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<SpanCopyable v-if="getValue(networkInfo)" :value="getValue(networkInfo)" />
|
||||||
|
<n-text v-else depth="3">
|
||||||
|
{{ undefinedFallback }}
|
||||||
|
</n-text>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</n-table>
|
||||||
|
|
||||||
|
<div mt-3 flex items-center justify-between>
|
||||||
|
<c-button @click="switchToBlock({ count: -1 })">
|
||||||
|
<n-icon :component="ArrowLeft" />
|
||||||
|
Previous block
|
||||||
|
</c-button>
|
||||||
|
<c-button @click="switchToBlock({ count: 1 })">
|
||||||
|
Next block
|
||||||
|
<n-icon :component="ArrowRight" />
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
|
@ -1,34 +1,3 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-space vertical :size="50">
|
|
||||||
<n-alert title="Info" type="info">
|
|
||||||
This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed,
|
|
||||||
and the lower 40 bits to generate your random ULA.
|
|
||||||
</n-alert>
|
|
||||||
|
|
||||||
<n-form-item label="MAC address:" v-bind="validationAttrs">
|
|
||||||
<n-input
|
|
||||||
v-model:value="macAddress"
|
|
||||||
size="large"
|
|
||||||
placeholder="Type a MAC address"
|
|
||||||
clearable
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
</n-space>
|
|
||||||
|
|
||||||
<div v-if="validationAttrs.validationStatus !== 'error'">
|
|
||||||
<n-input-group v-for="{ label, value } in calculatedSections" :key="label" style="margin: 5px 0">
|
|
||||||
<n-input-group-label style="flex: 0 0 160px"> {{ label }} </n-input-group-label>
|
|
||||||
<input-copyable :value="value" readonly />
|
|
||||||
</n-input-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SHA1 } from 'crypto-js';
|
import { SHA1 } from 'crypto-js';
|
||||||
import InputCopyable from '@/components/InputCopyable.vue';
|
import InputCopyable from '@/components/InputCopyable.vue';
|
||||||
|
@ -41,7 +10,7 @@ const calculatedSections = computed(() => {
|
||||||
.toString()
|
.toString()
|
||||||
.substring(30);
|
.substring(30);
|
||||||
|
|
||||||
const ula = 'fd' + hex40bit.substring(0, 2) + ':' + hex40bit.substring(2, 6) + ':' + hex40bit.substring(6);
|
const ula = `fd${hex40bit.substring(0, 2)}:${hex40bit.substring(2, 6)}:${hex40bit.substring(6)}`;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -59,7 +28,38 @@ const calculatedSections = computed(() => {
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
const { attrs: validationAttrs } = macAddressValidation(macAddress);
|
const addressValidation = macAddressValidation(macAddress);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-alert title="Info" type="info">
|
||||||
|
This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed,
|
||||||
|
and the lower 40 bits to generate your random ULA.
|
||||||
|
</n-alert>
|
||||||
|
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="macAddress"
|
||||||
|
placeholder="Type a MAC address"
|
||||||
|
clearable
|
||||||
|
label="MAC address:"
|
||||||
|
raw-text
|
||||||
|
my-8
|
||||||
|
:validation="addressValidation"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="addressValidation.isValid">
|
||||||
|
<InputCopyable
|
||||||
|
v-for="{ label, value } in calculatedSections"
|
||||||
|
:key="label"
|
||||||
|
:value="value"
|
||||||
|
:label="label"
|
||||||
|
label-width="160px"
|
||||||
|
label-align="right"
|
||||||
|
label-position="left"
|
||||||
|
readonly
|
||||||
|
mb-2
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue