Merge remote-tracking branch 'CorentinTh/main' into main

This commit is contained in:
marvin-j97 2023-06-01 22:48:33 +02:00
commit 4447ffee3c
207 changed files with 7280 additions and 3237 deletions

View file

@ -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,

View file

@ -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:'] }],
}, },
}; };

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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
View file

@ -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
View file

@ -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']
} }
} }

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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">

View file

@ -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>

View file

@ -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;

View file

@ -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);

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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) {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -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 />&nbsp; <br>&nbsp;
</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 {

View file

@ -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);
} }

View file

@ -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 {

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);
}); });

View file

@ -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';

View file

@ -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: {

View file

@ -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;

View file

@ -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>

View file

@ -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());

View 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');
}

View file

@ -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 });

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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);

View file

@ -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
View file

@ -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;
}

View file

@ -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,

View file

@ -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%;
} }

View file

@ -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,

View file

@ -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>

View file

@ -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;

View file

@ -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');

View file

@ -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}`;
} }

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View 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'),
});

View 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,
};
}

View file

@ -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;

View file

@ -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', () => {

View file

@ -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;

View file

@ -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;

View file

@ -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>

View file

@ -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 {

View file

@ -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 }) => {

View file

@ -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', () => {

View file

@ -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;
} }
} }

View file

@ -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
}; }

View file

@ -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>

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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 {

View file

@ -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;

View file

@ -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('');
} }

View file

@ -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>

View file

@ -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>

View file

@ -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('&lt;title&gt;IT Tool&lt;/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('&lt;title&gt;IT Tool&lt;/title');
const unescapeOutput = computed(() => unescape(unescapeInput.value));
const { copy: copyUnescaped } = useCopy({ source: unescapeOutput });
</script>

View file

@ -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;
} }

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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',
}, },

View file

@ -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 }) => {

View file

@ -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>

View file

@ -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 })),
); );

View file

@ -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', () => {

View file

@ -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) {

View file

@ -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;

View file

@ -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', () => {

View file

@ -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()
); );

View file

@ -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>

View 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'),
});

View file

@ -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();
});
});

View file

@ -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();
});
});
});

View 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;
}

View file

@ -0,0 +1,7 @@
export interface Ipv4RangeExpanderResult {
oldSize?: number
newStart?: string
newEnd?: string
newCidr?: string
newSize?: number
}

View 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">
&nbsp;
</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>

View 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>

View file

@ -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>

View file

@ -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