mirror of
				https://github.com/CorentinTh/it-tools.git
				synced 2025-10-31 12:03:48 +00:00 
			
		
		
		
	Compare commits
	
		
			143 Commits
		
	
	
		
			v2023.4.13
			...
			card-hover
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | e9e0884789 | ||
|  | 0eedce69a6 | ||
|  | e0c7771e8f | ||
|  | 8a30b6bdb3 | ||
|  | 233d5565f6 | ||
|  | 7ab9204e96 | ||
|  | c7d4562d3b | ||
|  | 18dd1400bd | ||
|  | f035f485c0 | ||
|  | e18bae1fca | ||
|  | d1dff428d8 | ||
|  | 3a63837d3d | ||
|  | 81bfe57cb8 | ||
|  | a9cd91ca9c | ||
|  | 06c35472d3 | ||
|  | f3e14fc18f | ||
|  | 2274766a8f | ||
|  | a346175d24 | ||
|  | 6f93cba3da | ||
|  | 76b2761d62 | ||
|  | 6ff9a01cc8 | ||
|  | a2b9b157e5 | ||
|  | 144f86e2dc | ||
|  | 0f1f6590c5 | ||
|  | 2bcb77a9f9 | ||
|  | c58d6e3423 | ||
|  | f235dcd6c1 | ||
|  | ba2c589f0f | ||
|  | 9bd4ad4dfd | ||
|  | 5e12991bcd | ||
|  | 65a9474078 | ||
|  | 85cc7a8447 | ||
|  | 4268e255de | ||
|  | d1c888019b | ||
|  | 7924456cec | ||
|  | 99bc84c37e | ||
|  | ea0f27cf4c | ||
|  | cd5a503fc0 | ||
|  | 86e964a274 | ||
|  | 7b6232a151 | ||
|  | 56d74d07a8 | ||
|  | e5d0ba7073 | ||
|  | 1a602365be | ||
|  | a2494219a8 | ||
|  | 93f7cf0e98 | ||
|  | dfa1ba8554 | ||
|  | 6498c9b0fa | ||
|  | f40d7ecddf | ||
|  | 2e28c5073e | ||
|  | d12dd40841 | ||
|  | cf382b5a10 | ||
|  | 01525838e0 | ||
|  | 38cb61da4d | ||
|  | 354aed6e6f | ||
|  | 6b8682fd23 | ||
|  | 9e8349dbc4 | ||
|  | f44050742d | ||
|  | b0d9a3e6c7 | ||
|  | 30f88fc6a8 | ||
|  | 72c98a3c5e | ||
|  | 05ea545475 | ||
|  | 5c3bebfe62 | ||
|  | 8df7cd0f19 | ||
|  | a9c7b89193 | ||
|  | 6bda2caa04 | ||
|  | 994a1c3401 | ||
|  | 05edaf423c | ||
|  | 8c72e692a7 | ||
|  | cec9dea9e0 | ||
|  | 49eacea195 | ||
|  | 3f7d469e9f | ||
|  | 5f2190887d | ||
|  | 6cb0845336 | ||
|  | 12d9e5d377 | ||
|  | e29b258e90 | ||
|  | ea50a3fc65 | ||
|  | 746e5bdccc | ||
|  | c7d4f112c0 | ||
|  | 9125dcf9c6 | ||
|  | 38710dce56 | ||
|  | 282cfc4c4b | ||
|  | ba4876d0d5 | ||
|  | 363c2e47e6 | ||
|  | 9526ed8324 | ||
|  | 7068610438 | ||
|  | 847323ccba | ||
|  | 1b038c7826 | ||
|  | ec4c533718 | ||
|  | 63045951e1 | ||
|  | c4cec9e18f | ||
|  | bcb98b359c | ||
|  | 732da08157 | ||
|  | b9406a492d | ||
|  | 69f0bd079f | ||
|  | 4cbd7ac145 | ||
|  | ebfb872fae | ||
|  | a6bbeaebd8 | ||
|  | f771e7a99f | ||
|  | cf7b1f000a | ||
|  | 1e2a35b892 | ||
|  | 45c2474279 | ||
|  | fe61f0f2f2 | ||
|  | 93799af83c | ||
|  | 962a6d6ec4 | ||
|  | d2956b66fe | ||
|  | 105b21badc | ||
|  | 33c9b6643f | ||
|  | 4d2b037dbe | ||
|  | 34d8e5ce2c | ||
|  | 8515c24264 | ||
|  | 0b20f1c16a | ||
|  | 8c92d56318 | ||
|  | 7aed9c56fd | ||
|  | f7fc779e63 | ||
|  | b3b6b7c46b | ||
|  | 141c12455e | ||
|  | 77f2efc0b9 | ||
|  | aad8d84e13 | ||
|  | 401f13f7e3 | ||
|  | edae4c6915 | ||
|  | a43c546e34 | ||
|  | 83a7b3bae9 | ||
|  | ce3150c65d | ||
|  | 3f6c8f0edd | ||
|  | daf2cf0285 | ||
|  | b7aaea1b58 | ||
|  | 92bd83536f | ||
|  | e88c1d5f2c | ||
|  | 362f2fa280 | ||
|  | 61ece2387f | ||
|  | f080933d2a | ||
|  | bb32513bd3 | ||
|  | c311e3824d | ||
|  | 74073f5038 | ||
|  | c45bce36f9 | ||
|  | df989e24b3 | ||
|  | 6d2202597c | ||
|  | c68a1fd713 | ||
|  | 46b1a07213 | ||
|  | dbad7730f9 | ||
|  | 85cb0ffabd | ||
|  | 8355bd2ae4 | ||
|  | 6fb4994603 | 
| @@ -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, | ||||||
| @@ -107,6 +106,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 +129,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, | ||||||
| @@ -160,6 +172,7 @@ | |||||||
|     "useFullscreen": true, |     "useFullscreen": true, | ||||||
|     "useGamepad": true, |     "useGamepad": true, | ||||||
|     "useGeolocation": true, |     "useGeolocation": true, | ||||||
|  |     "useI18n": true, | ||||||
|     "useIdle": true, |     "useIdle": true, | ||||||
|     "useImage": true, |     "useImage": true, | ||||||
|     "useInfiniteScroll": true, |     "useInfiniteScroll": 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, | ||||||
| @@ -259,6 +285,7 @@ | |||||||
|     "watchThrottled": true, |     "watchThrottled": true, | ||||||
|     "watchTriggerable": true, |     "watchTriggerable": true, | ||||||
|     "watchWithFilter": true, |     "watchWithFilter": true, | ||||||
|     "whenever": true |     "whenever": true, | ||||||
|  |     "toValue": true | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -1,39 +1,21 @@ | |||||||
| /* 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': [ |     'no-restricted-imports': ['error', { | ||||||
|       'error', |       paths: [{ | ||||||
|       'ignorePackages', |         name: '@vueuse/core', | ||||||
|       { |         importNames: ['useClipboard'], | ||||||
|         js: 'never', |         message: 'Please use local useCopy from src/composable/copy.ts instead of useClipboard.', | ||||||
|         ts: 'never', |       }], | ||||||
|         tsx: 'never', |     }], | ||||||
|       }, |  | ||||||
|     ], |  | ||||||
|     'import/no-unresolved': ['error', { ignore: ['^virtual:'] }], |  | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/ISSUE_TEMPLATE/new-tool-request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/ISSUE_TEMPLATE/new-tool-request.md
									
									
									
									
										vendored
									
									
								
							| @@ -6,8 +6,8 @@ labels: new tool | |||||||
| assignees: CorentinTh | assignees: CorentinTh | ||||||
| --- | --- | ||||||
|  |  | ||||||
| **Which tool is impacted?** | **What tool do you want?** | ||||||
| Example: the token generator | Example: a token generator | ||||||
|  |  | ||||||
| **Describe the solution you'd like** | **Describe the solution you'd like** | ||||||
| A clear and concise description of what you want to happen. | A clear and concise description of what you want to happen. | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -27,5 +27,8 @@ jobs: | |||||||
|       - name: Run unit test |       - name: Run unit test | ||||||
|         run: pnpm test |         run: pnpm test | ||||||
|  |  | ||||||
|  |       - name: Type check | ||||||
|  |         run: pnpm typecheck | ||||||
|  |  | ||||||
|       - name: Build the app |       - name: Build the app | ||||||
|         run: pnpm build |         run: pnpm build | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -37,11 +37,11 @@ jobs: | |||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|     - name: Checkout repository |     - name: Checkout repository | ||||||
|       uses: actions/checkout@v2 |       uses: actions/checkout@v3 | ||||||
|  |  | ||||||
|     # Initializes the CodeQL tools for scanning. |     # Initializes the CodeQL tools for scanning. | ||||||
|     - name: Initialize CodeQL |     - name: Initialize CodeQL | ||||||
|       uses: github/codeql-action/init@v1 |       uses: github/codeql-action/init@v2 | ||||||
|       with: |       with: | ||||||
|         languages: ${{ matrix.language }} |         languages: ${{ matrix.language }} | ||||||
|         # If you wish to specify custom queries, you can do so here or in a config file. |         # If you wish to specify custom queries, you can do so here or in a config file. | ||||||
| @@ -52,7 +52,7 @@ jobs: | |||||||
|     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). |     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). | ||||||
|     # If this step fails, then you should remove it and run the build manually (see below) |     # If this step fails, then you should remove it and run the build manually (see below) | ||||||
|     - name: Autobuild |     - name: Autobuild | ||||||
|       uses: github/codeql-action/autobuild@v1 |       uses: github/codeql-action/autobuild@v2 | ||||||
|  |  | ||||||
|     # ℹ️ Command-line programs to run using the OS shell. |     # ℹ️ Command-line programs to run using the OS shell. | ||||||
|     # 📚 https://git.io/JvXDl |     # 📚 https://git.io/JvXDl | ||||||
| @@ -66,4 +66,4 @@ jobs: | |||||||
|     #   make release |     #   make release | ||||||
|  |  | ||||||
|     - name: Perform CodeQL Analysis |     - name: Perform CodeQL Analysis | ||||||
|       uses: github/codeql-action/analyze@v1 |       uses: github/codeql-action/analyze@v2 | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/docker-nightly-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/docker-nightly-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -12,7 +12,7 @@ jobs: | |||||||
|     outputs: |     outputs: | ||||||
|       should_run: ${{ steps.should_run.outputs.should_run }} |       should_run: ${{ steps.should_run.outputs.should_run }} | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v3 | ||||||
|       - name: print latest_commit |       - name: print latest_commit | ||||||
|         run: echo ${{ github.sha }} |         run: echo ${{ github.sha }} | ||||||
|  |  | ||||||
| @@ -57,7 +57,7 @@ jobs: | |||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@v3 | ||||||
|  |  | ||||||
|       - name: Login to GitHub Container Registry |       - name: Login to GitHub Container Registry | ||||||
|         uses: docker/login-action@v1 |         uses: docker/login-action@v2 | ||||||
|         with: |         with: | ||||||
|           registry: ghcr.io |           registry: ghcr.io | ||||||
|           username: ${{ github.repository_owner }} |           username: ${{ github.repository_owner }} | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | name: E2E tests | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  | jobs: | ||||||
|  |   test: | ||||||
|  |     timeout-minutes: 60 | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     strategy: | ||||||
|  |       matrix: | ||||||
|  |         shard: [1/3, 2/3, 3/3] | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v3 | ||||||
|  |  | ||||||
|  |       - run: corepack enable | ||||||
|  |  | ||||||
|  |       - uses: actions/setup-node@v3 | ||||||
|  |         with: | ||||||
|  |           node-version: 16 | ||||||
|  |           cache: 'pnpm' | ||||||
|  |  | ||||||
|  |       - name: Get Playwright version | ||||||
|  |         id: playwright-version | ||||||
|  |         run: echo "PLAYWRIGHT_VERSION=$(jq -r .dependencies.playwright package.json)" >> "$GITHUB_OUTPUT" | ||||||
|  |  | ||||||
|  |       - name: Install dependencies | ||||||
|  |         run: pnpm install | ||||||
|  |  | ||||||
|  |       - name: Build app | ||||||
|  |         run: pnpm build | ||||||
|  |  | ||||||
|  |       - name: Restore Playwright browsers from cache | ||||||
|  |         uses: actions/cache@v3 | ||||||
|  |         with: | ||||||
|  |           path: ~/.cache/ms-playwright | ||||||
|  |           key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-${{ hashFiles('**/playwright.config.ts') }} | ||||||
|  |           restore-keys: | | ||||||
|  |             ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}- | ||||||
|  |             ${{ runner.os }}-playwright- | ||||||
|  |  | ||||||
|  |       - name: Install Playwright Browsers | ||||||
|  |         run: pnpm exec playwright install --with-deps | ||||||
|  |  | ||||||
|  |       - name: Run Playwright tests | ||||||
|  |         run: pnpm run test:e2e --shard=${{ matrix.shard }} | ||||||
							
								
								
									
										23
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,23 +0,0 @@ | |||||||
| name: E2E tests |  | ||||||
| on: |  | ||||||
|   pull_request: |  | ||||||
|   push: |  | ||||||
|     branches: |  | ||||||
|       - main |  | ||||||
| jobs: |  | ||||||
|   test: |  | ||||||
|     timeout-minutes: 60 |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v3 |  | ||||||
|       - run: corepack enable |  | ||||||
|       - uses: actions/setup-node@v3 |  | ||||||
|         with: |  | ||||||
|           node-version: 16 |  | ||||||
|           cache: 'pnpm' |  | ||||||
|       - name: Install dependencies |  | ||||||
|         run: pnpm install |  | ||||||
|       - name: Install Playwright Browsers |  | ||||||
|         run: pnpm exec playwright install --with-deps |  | ||||||
|       - name: Run Playwright tests |  | ||||||
|         run: pnpm exec playwright test |  | ||||||
							
								
								
									
										58
									
								
								.github/workflows/releases.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										58
									
								
								.github/workflows/releases.yml
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,7 @@ jobs: | |||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@v3 | ||||||
|  |  | ||||||
|       - name: Login to GitHub Container Registry |       - name: Login to GitHub Container Registry | ||||||
|         uses: docker/login-action@v1 |         uses: docker/login-action@v2 | ||||||
|         with: |         with: | ||||||
|           registry: ghcr.io |           registry: ghcr.io | ||||||
|           username: ${{ github.repository_owner }} |           username: ${{ github.repository_owner }} | ||||||
| @@ -46,3 +46,59 @@ jobs: | |||||||
|             corentinth/it-tools:${{ env.RELEASE_VERSION }} |             corentinth/it-tools:${{ env.RELEASE_VERSION }} | ||||||
|             ghcr.io/corentinth/it-tools:latest |             ghcr.io/corentinth/it-tools:latest | ||||||
|             ghcr.io/corentinth/it-tools:${{ env.RELEASE_VERSION}} |             ghcr.io/corentinth/it-tools:${{ env.RELEASE_VERSION}} | ||||||
|  |  | ||||||
|  |   github-release: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: docker-release | ||||||
|  |     steps: | ||||||
|  |       - name: Get release version | ||||||
|  |         run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV | ||||||
|  |  | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |  | ||||||
|  |       - run: corepack enable | ||||||
|  |  | ||||||
|  |       - uses: actions/setup-node@v3 | ||||||
|  |         with: | ||||||
|  |           node-version: 16 | ||||||
|  |           cache: 'pnpm' | ||||||
|  |  | ||||||
|  |       - name: Install dependencies | ||||||
|  |         run: pnpm i | ||||||
|  |  | ||||||
|  |       - name: Build the app | ||||||
|  |         run: pnpm build | ||||||
|  |  | ||||||
|  |       - name: Zip the app | ||||||
|  |         run: zip -r it-tools-${{ env.RELEASE_VERSION }}.zip dist/* | ||||||
|  |  | ||||||
|  |       - name: Get changelog | ||||||
|  |         id: changelog | ||||||
|  |         run: | | ||||||
|  |           EOF=$(openssl rand -hex 8) | ||||||
|  |           echo "changelog<<$EOF" >> $GITHUB_OUTPUT | ||||||
|  |           node ./scripts/getLatestChangelog.mjs >> $GITHUB_OUTPUT | ||||||
|  |           echo "$EOF" >> $GITHUB_OUTPUT | ||||||
|  |  | ||||||
|  |       - name: Create Release | ||||||
|  |         uses: softprops/action-gh-release@v1 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |           files: it-tools-${{ env.RELEASE_VERSION }}.zip | ||||||
|  |           tag_name: v${{ env.RELEASE_VERSION }} | ||||||
|  |           draft: true | ||||||
|  |           prerelease: false | ||||||
|  |           body: | | ||||||
|  |             ## Docker images | ||||||
|  |  | ||||||
|  |             - Docker Hub | ||||||
|  |               - `corentinth/it-tools:latest` | ||||||
|  |               - `corentinth/it-tools:${{ env.RELEASE_VERSION }}` | ||||||
|  |             - GitHub Container Registry | ||||||
|  |               - `ghcr.io/corentinth/it-tools:latest` | ||||||
|  |               - `ghcr.io/corentinth/it-tools:${{ env.RELEASE_VERSION}}` | ||||||
|  |  | ||||||
|  |             ## Changelog | ||||||
|  |  | ||||||
|  |             ${{ steps.changelog.outputs.changelog }} | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -31,3 +31,5 @@ coverage | |||||||
| /test-results/ | /test-results/ | ||||||
| /playwright-report/ | /playwright-report/ | ||||||
| /playwright/.cache/ | /playwright/.cache/ | ||||||
|  | # Webkit with playwright creates a salt file | ||||||
|  | salt | ||||||
							
								
								
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,3 @@ | |||||||
| { | { | ||||||
|   "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin", "dbaeumer.vscode-eslint"] |   "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin", "dbaeumer.vscode-eslint", "lokalise.i18n-ally"] | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										696
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										696
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -2,577 +2,211 @@ | |||||||
|  |  | ||||||
| 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. | ||||||
|  |  | ||||||
| ## [2.19.0](https://github.com/CorentinTh/it-tools/compare/v2.18.0...v2.19.0) (2023-02-06) | ## Version 2023.08.21-6f93cba | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features | ### Features | ||||||
|  | - **copy**: support legacy copy to clipboard for older browser (#581) (6f93cba) | ||||||
|  | - **new tool**: string obfuscator (#575) (c58d6e3) | ||||||
|  |  | ||||||
| * **new-tool:** keycode info ([c934c4e](https://github.com/CorentinTh/it-tools/commit/c934c4e50ca1a129b80b786a5d9a7dbc33ad9ba3)) | ### Bug fixes | ||||||
|  | - **deps**: update dependency sql-formatter to v12 (#520) (2bcb77a) | ||||||
|  |  | ||||||
| ## [2.18.0](https://github.com/CorentinTh/it-tools/compare/v2.17.0...v2.18.0) (2023-02-04) | ### Chores | ||||||
|  | - **deps**: switched to fucking typescript v5 (#501) (76b2761) | ||||||
|  | - **deps**: update dependency @antfu/eslint-config to ^0.40.0 (#552) (6ff9a01) | ||||||
|  | - **deps**: update dependency prettier to v3 (#564) (a2b9b15) | ||||||
|  | - **deps**: removed @typescript-eslint/parser (#563) (144f86e) | ||||||
|  | - **deps**: removed ts-pattern (#565) (0f1f659) | ||||||
|  |  | ||||||
|  | ## Version 2023.08.16-9bd4ad4 | ||||||
|  |  | ||||||
| ### Features | ### Features | ||||||
|  | - **Case Converter**: Add lowercase and uppercase (#534) (7b6232a) | ||||||
| * **new-tool:** json minify ([#265](https://github.com/CorentinTh/it-tools/issues/265)) ([f708f50](https://github.com/CorentinTh/it-tools/commit/f708f5091e2182fc88e7cf3e7d23b3d05edc04da)) | - **new tool**: emoji picker (#551) (93f7cf0) | ||||||
|  | - **ui**: added c-select in the ui lib (#550) (dfa1ba8) | ||||||
|  | - **new-tool**: password strength analyzer (#502) (a9c7b89) | ||||||
| ### Refactors | - **new-tool**: yaml to toml (e29b258) | ||||||
|  | - **new-tool**: json to toml (ea50a3f) | ||||||
| * **tools:** config in query params ([db817a2](https://github.com/CorentinTh/it-tools/commit/db817a2459e23bd096274a7f91815d613d5f7ff4)) | - **new-tool**: toml to yaml (746e5bd) | ||||||
|  | - **new-tool**: toml to json (c7d4f11) | ||||||
| ## [2.17.0](https://github.com/CorentinTh/it-tools/compare/v2.16.0...v2.17.0) (2023-01-13) | - **command-palette**: random tool action (ec4c533) | ||||||
|  | - **config**: allow app to run in a subfolder via BASE_URL (#461) (6304595) | ||||||
|  | - **new-tool**: percentage calculator (#456) (b9406a4) | ||||||
| ### Features | - **new-tool**: json to csv converter (69f0bd0) | ||||||
|  | - **new tool**: xml formatter (#457) (a6bbeae) | ||||||
| * **new-tool:** jwt parser ([#262](https://github.com/CorentinTh/it-tools/issues/262)) ([acc7f0a](https://github.com/CorentinTh/it-tools/commit/acc7f0a586c64500c5f720e70cdbccf9bffe76d9)) | - **chmod-calculator**: added symbolic representation (#455) (f771e7a) | ||||||
| * **new-tool:** temperature converter ([4607837](https://github.com/CorentinTh/it-tools/commit/4607837f9a398440e0098f2ba862e8d7422ce94f)) | - **enhancement**: use system dark mode (#458) (cf7b1f0) | ||||||
|  | - **phone-parser**: searchable country code select (d2956b6) | ||||||
|  | - **new tool**: camera screenshot and recorder (34d8e5c) | ||||||
| ### Refactors | - **base64-string-converter**: switch to encode and decode url safe base64 strings (#392) (0b20f1c) | ||||||
|  |  | ||||||
| * **jwt-parser:** simplified code ([f52f7a8](https://github.com/CorentinTh/it-tools/commit/f52f7a845c34ce7da57b11c17d261733be89554f)) | ### Bug fixes | ||||||
|  | - **deps**: update dependency uuid to v9 (#566) (5e12991) | ||||||
| ## [2.16.0](https://github.com/CorentinTh/it-tools/compare/v2.15.0...v2.16.0) (2022-12-21) | - **deps**: update dependency mathjs to v11 (#519) (7924456) | ||||||
|  | - **deps**: update dependency @vueuse/router to v10 (#516) (ea0f27c) | ||||||
|  | - **copy**: prevent shorthand copy if source is present in useCopy (#559) (86e964a) | ||||||
| ### Features | - **c-lib**: hide component library shortcut link in non-dev (#557) (56d74d0) | ||||||
|  | - **emoji picker**: fix copy button (#556) (e5d0ba7) | ||||||
| * **search-bar:** use cmd + k to focus on mac ([bf88836](https://github.com/CorentinTh/it-tools/commit/bf88836dbe4037019e9545deaae1db06e5768cfb)) | - **deps**: update dependency @vueuse/head to v1 (#515) (d12dd40) | ||||||
| * **tool:** improved favorite tool management ([af075dc](https://github.com/CorentinTh/it-tools/commit/af075dccccec959a0863e6d11516206860bed91f)) | - **deps**: update dependency country-code-lookup to ^0.1.0 (#493) (8c72e69) | ||||||
| * **tools:** added favorite tool handling ([4cd809b](https://github.com/CorentinTh/it-tools/commit/4cd809bd0c94836532f58a2ec6aa131694cce10d)) | - **deps**: update dependency @vueuse/head to ^0.9.0 (#492) (cec9dea) | ||||||
| * **tracker:** added actions monitoring ([bfc2e24](https://github.com/CorentinTh/it-tools/commit/bfc2e24bbfc08f67ed9c9b1d93474029bc01dc8b)) | - **i18n**: fallback for demo i18n (12d9e5d) | ||||||
|  | - **typos**: fixed more typos & uppercase JSON (#475) (9526ed8) | ||||||
|  | - **about**: typos and wording (#474) (7068610) | ||||||
| ### Refactors | - **mime-types**: typos (#470) (c4cec9e) | ||||||
|  | - **sonar**: took down minor sonar warning (4cbd7ac) | ||||||
| * **clean:** removed empty style tag ([cf723f1](https://github.com/CorentinTh/it-tools/commit/cf723f144ee865b6de7323d3be58eb7a9586fa56)) | - **readme**: typo (105b21b) | ||||||
| * **clean:** removed unused import ([4087285](https://github.com/CorentinTh/it-tools/commit/40872859a580a20bb838b79db2b3c88c00995e37)) | - **ipv4-range-expander**: calculate correct for ip addresses where the first octet is lower than 128 (#405) (8c92d56) | ||||||
| * **menu:** improve support button ([679dd1c](https://github.com/CorentinTh/it-tools/commit/679dd1c1f6265227cc9db60c55d83f8eaf8f72b4)) | - **ipv4-converter**: removed readonly on input (7aed9c5) | ||||||
| * **tracker:** better tracker injection ([def60e7](https://github.com/CorentinTh/it-tools/commit/def60e7248003e74ed67e9ff116b438bab410a92)) |  | ||||||
|  | ### Refactoring | ||||||
| ## [2.15.0](https://github.com/CorentinTh/it-tools/compare/v2.14.1...v2.15.0) (2022-12-16) | - **navbar**: consistent spacing in navbar buttons (#507) (30f88fc) | ||||||
|  | - **ui**: remove n-text (#506) (72c98a3) | ||||||
|  | - **ui**: replaced some n-input to c-input (#505) (05ea545) | ||||||
| ### Features | - **json-viewer**: input monospace font (#485) (9125dcf) | ||||||
|  | - **search**: command palette design (#463) (bcb98b3) | ||||||
| * **search-bar:** better search back result ([71e98e9](https://github.com/CorentinTh/it-tools/commit/71e98e93e5752cba934f67d679088524c4d3d2ad)) | - **c-input-text**: force usage of props with default (1e2a35b) | ||||||
|  | - **naming**: prevent auto import conflicts for git memo (45c2474) | ||||||
|  | - **imports**: removed unnecessary imports to vue (fe61f0f) | ||||||
| ### Bug Fixes | - **ui**: removed all n-space (4d2b037) | ||||||
|  | - **ui**: replaced some n-input with c-input-text (f7fc779) | ||||||
| * **integer-base-converter:** handle non-decimal char and better error message ([8476cf3](https://github.com/CorentinTh/it-tools/commit/8476cf319b7ebae87c7928592604a54833ac56ef)) |  | ||||||
| * **tool-card:** correct text color on light mode for card description ([acf8bc1](https://github.com/CorentinTh/it-tools/commit/acf8bc11dbab85ab361edbe400ebbe5e52a11b89)) | ### Chores | ||||||
|  | - **deps**: update dependency vitest to ^0.34.0 (#562) (9bd4ad4) | ||||||
|  | - **deps**: update dependency node to v18.17.1 (#560) (65a9474) | ||||||
| ### Refactors | - **deps**: update dependency unocss to ^0.55.0 (#561) (85cc7a8) | ||||||
|  | - **deps**: update dependency @unocss/eslint-config to ^0.55.0 (#553) (4268e25) | ||||||
| * **search-bar:** improved tool fuzzy search ([1b5d4e7](https://github.com/CorentinTh/it-tools/commit/1b5d4e72bdb222dd721a1e484c3e5d73bb62d2b1)) | - **deps**: update dependency @intlify/unplugin-vue-i18n to ^0.12.0 (#526) (d1c8880) | ||||||
|  | - **deps**: update docker/login-action action to v2 (#512) (99bc84c) | ||||||
| ### [2.14.1](https://github.com/CorentinTh/it-tools/compare/v2.14.0...v2.14.1) (2022-11-23) | - **deps**: update dependency jsdom to v22 (#499) (cd5a503) | ||||||
|  | - **deps**: update dependency @vitejs/plugin-vue-jsx to v3 (#497) (1a60236) | ||||||
| ## [2.14.0](https://github.com/CorentinTh/it-tools/compare/v2.13.0...v2.14.0) (2022-11-23) | - **deps**: update dependency @vitejs/plugin-vue to v4 (#496) (a249421) | ||||||
|  | - **deps**: update dependency vite-plugin-pwa to ^0.16.0 (#488) (6498c9b) | ||||||
|  | - **deps**: update dependency vite to v4 (#503) (f40d7ec) | ||||||
| ### Features | - **ci**: e2e against vercel deployement (#518) (2e28c50) | ||||||
|  | - **e2e**: execute e2e against built app (#511) (cf382b5) | ||||||
| * **new-tool:** chmod calculator ([35b5187](https://github.com/CorentinTh/it-tools/commit/35b518711938c2bc88f35d104bb35d9956f0c267)) | - **deps**: update github/codeql-action action to v2 (#513) (0152583) | ||||||
|  | - **deps**: update node.js to v18 (#514) (38cb61d) | ||||||
| ## [2.13.0](https://github.com/CorentinTh/it-tools/compare/v2.11.0...v2.13.0) (2022-11-14) | - **deps**: switched from vite-plugin-md to vite-plugin-vue-markdown (#510) (354aed6) | ||||||
|  | - **deps**: update dependency workbox-window to v7 (#509) (6b8682f) | ||||||
|  | - **deps**: update dependency vite-svg-loader to v4 (#508) (9e8349d) | ||||||
| ### Features | - **deps**: update dependency typescript to ~4.9.0 (#481) (f440507) | ||||||
|  | - **deps**: update dependency vue-tsc to ^0.40.0 (#490) (b0d9a3e) | ||||||
| * **config:** added tsx to allowed extension ([ea5e7a7](https://github.com/CorentinTh/it-tools/commit/ea5e7a7fc7df1a3a912193912a6ab80a8a36a256)) | - **deps**: updated unplugin-auto-import (#504) (5c3bebf) | ||||||
| * **date-converter:** added mongodb objectID format ([4ef2588](https://github.com/CorentinTh/it-tools/commit/4ef25887b9d874b8789bf8dbabd8aab92b4b1b03)) | - **deps**: removed start-server-and-test dependency (8df7cd0) | ||||||
| * **new-tool:** added otp generator ([5f16885](https://github.com/CorentinTh/it-tools/commit/5f168859238e9c3a8b8bbaf6b550c4b9bd163e00)) | - **deps**: update dependency c8 to v8 (#498) (6bda2ca) | ||||||
| * **new-tool:** mime type to extension converter ([7c9b8ac](https://github.com/CorentinTh/it-tools/commit/7c9b8ac178967151a4f921ac26e8c2fe8d23b886)) | - **deps**: update dependency @types/jsdom to v21 (#495) (994a1c3) | ||||||
|  | - **deps**: update node.js to v16.20.1 (#491) (05edaf4) | ||||||
|  | - **deps**: update dependency vitest to ^0.32.0 (#489) (49eacea) | ||||||
| ### Bug Fixes | - **deps**: update actions/checkout action to v3 (#494) (3f7d469) | ||||||
|  | - **deps**: update dependency unplugin-vue-components to ^0.25.0 (#484) (5f21908) | ||||||
| * **ui:** remove icon transparency overlap ([35a3760](https://github.com/CorentinTh/it-tools/commit/35a376077116dd65b21f9a0786d2ecfc14db6051)) | - **deps**: update dependency unplugin-auto-import to ^0.16.0 (#483) (6cb0845) | ||||||
|  | - **deps**: update dependency unocss to ^0.53.0 (#482) (38710dc) | ||||||
|  | - **deps**: update dependency @unocss/eslint-config to ^0.53.0 (#478) (282cfc4) | ||||||
| ### Refactors | - **deps**: added renovate.json (#477) (363c2e4) | ||||||
|  | - **i18n**: tool scoped locales (#471) (1b038c7) | ||||||
| * **otp-generator:** changed url ([7f22995](https://github.com/CorentinTh/it-tools/commit/7f229959d64b7a932f32753e3838d87a819a9192)) | - **wysiwyg-editor**: update tiptap dependencies (732da08) | ||||||
| * token generator can use a custom alphabet ([83da6b7](https://github.com/CorentinTh/it-tools/commit/83da6b7ee9db29e40faf288f9627257aa7124038)) | - **i18n**: setup i18n plugin config (ebfb872) | ||||||
| * **ui:** change sponsor button location and caption ([5d8f46a](https://github.com/CorentinTh/it-tools/commit/5d8f46abf8d5a10cc4650efc87b12a9a6c537fe5)) | - **config**: netlify deployment support (#443) (93799af) | ||||||
| * **useQRCode:** switched args to MaybeRef ([7de6c86](https://github.com/CorentinTh/it-tools/commit/7de6c86f9ead8d7315614cc508dfee4fed90e9c2)) | - **ci**: shard e2e tests (962a6d6) | ||||||
|  | - **lint**: switched to a better lint config (33c9b66) | ||||||
| ## [2.12.0](https://github.com/CorentinTh/it-tools/compare/v2.10.3...v2.12.0) (2022-08-23) |  | ||||||
|  | ### Refacor | ||||||
|  | - **transformers**: use monospace font for JSON and SQL text areas (#476) (ba4876d) | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| * added colored share card ([ab7483b](https://github.com/CorentinTh/it-tools/commit/ab7483b5c2bd5aee1b8b609597c22b7b7b55606d)) |  | ||||||
| * **config:** added tsx to allowed extension ([741a3c2](https://github.com/CorentinTh/it-tools/commit/741a3c25a915d8296987b23bda03f2b664d51ba6)) |  | ||||||
| * **new-tool:** added otp generator ([cc6070a](https://github.com/CorentinTh/it-tools/commit/cc6070a16655bce9de90517bdda3bf6224ba139d)) |  | ||||||
| * **new-tool:** meta tag generator ([164e32b](https://github.com/CorentinTh/it-tools/commit/164e32b4428b8dfaaddcefa06b767a8af94573a9)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **deps:** added missing optional deps ([4975590](https://github.com/CorentinTh/it-tools/commit/49755909bdaea9399e51b67fbd1a6d071acd3182)) |  | ||||||
| * removed colored card border ([7c449f4](https://github.com/CorentinTh/it-tools/commit/7c449f4f2d491ce58726c5419a74dc295fa92905)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **colored-card:** added transition on like hover ([da17696](https://github.com/CorentinTh/it-tools/commit/da17696293270005b1b7ec4aafc0df7496f602c7)) |  | ||||||
| * **share:** updated share meta ([5222bd5](https://github.com/CorentinTh/it-tools/commit/5222bd5d04ad089ba4cbade399dada55e29dcde5)) |  | ||||||
| * token generator can use a custom alphabet ([59ec629](https://github.com/CorentinTh/it-tools/commit/59ec6293b65526fe8dc527ac596d0e5af29b1e32)) |  | ||||||
| * **useQRCode:** switched args to MaybeRef ([a89c9be](https://github.com/CorentinTh/it-tools/commit/a89c9bea42d598f4caba10800becd66a07bbcdc9)) |  | ||||||
|  |  | ||||||
| ## [2.11.0](https://github.com/CorentinTh/it-tools/compare/v2.10.3...v2.11.0) (2022-08-19) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| * added colored share card ([ab7483b](https://github.com/CorentinTh/it-tools/commit/ab7483b5c2bd5aee1b8b609597c22b7b7b55606d)) |  | ||||||
| * **new-tool:** meta tag generator ([164e32b](https://github.com/CorentinTh/it-tools/commit/164e32b4428b8dfaaddcefa06b767a8af94573a9)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **deps:** added missing optional deps ([4975590](https://github.com/CorentinTh/it-tools/commit/49755909bdaea9399e51b67fbd1a6d071acd3182)) |  | ||||||
| * removed colored card border ([7c449f4](https://github.com/CorentinTh/it-tools/commit/7c449f4f2d491ce58726c5419a74dc295fa92905)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **colored-card:** added transition on like hover ([da17696](https://github.com/CorentinTh/it-tools/commit/da17696293270005b1b7ec4aafc0df7496f602c7)) |  | ||||||
| * **share:** updated share meta ([5222bd5](https://github.com/CorentinTh/it-tools/commit/5222bd5d04ad089ba4cbade399dada55e29dcde5)) |  | ||||||
|  |  | ||||||
| ### [2.10.3](https://github.com/CorentinTh/it-tools/compare/v2.10.2...v2.10.3) (2022-08-14) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **share:** new share banner ([fcf4cfe](https://github.com/CorentinTh/it-tools/commit/fcf4cfe64d4c1c3814137c8ff23b83a1ca0d502d)) |  | ||||||
| * **share:** updated twitter meta tags ([992f96b](https://github.com/CorentinTh/it-tools/commit/992f96b48a89e2793ccf75fb9e28b2ec7b7f62b6)) |  | ||||||
| * **validation:** simplified validation management with helpers ([f54223f](https://github.com/CorentinTh/it-tools/commit/f54223fb0aaedbd101b5d3dc4176053533bb936a)) |  | ||||||
|  |  | ||||||
| ### [2.10.2](https://github.com/CorentinTh/it-tools/compare/v2.10.1...v2.10.2) (2022-08-04) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **dry:** mutualised duplicated code with withDefaultOnError ([f6cd9b7](https://github.com/CorentinTh/it-tools/commit/f6cd9b76d38800e1a1f63d07152fc96cda562795)) |  | ||||||
| * **home:** removed new tool first sort ([d30cd8a](https://github.com/CorentinTh/it-tools/commit/d30cd8a9abc3298c0a0b05f249e54318bb4537f2)) |  | ||||||
| * **json-prettifier:** more permissive json parser ([8089c60](https://github.com/CorentinTh/it-tools/commit/8089c60000000c42c821c6586c128d3d2b248885)) |  | ||||||
| * **lint:** added import rules ([208a373](https://github.com/CorentinTh/it-tools/commit/208a373fd08ac550778745eb6e4536bf02537da7)) |  | ||||||
|  |  | ||||||
| ### [2.10.1](https://github.com/CorentinTh/it-tools/compare/v2.10.0...v2.10.1) (2022-08-04) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **bip39-generator:** cleared an issue with the mnemonic validation ([ca7cb44](https://github.com/CorentinTh/it-tools/commit/ca7cb4438972ca09f28a6a40332ec94ceaa4aab4)) |  | ||||||
| * **import:** removed auto added weird .js extension ([fda0b0c](https://github.com/CorentinTh/it-tools/commit/fda0b0ca25c1733542a4e797ac1a2150c546a660)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **base64:** mutualized base64 functions into global utilities ([447bdf2](https://github.com/CorentinTh/it-tools/commit/447bdf2148098d70ba309e13d9b1e846b5064da1)) |  | ||||||
| * **chronometer:** improved chronometer precision ([e48d60b](https://github.com/CorentinTh/it-tools/commit/e48d60b1ed19279f48441743f7ed69e8fd915011)) |  | ||||||
|  |  | ||||||
| ## [2.10.0](https://github.com/CorentinTh/it-tools/compare/v2.9.2...v2.10.0) (2022-08-03) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| * **hash-text:** digest base selector ([#254](https://github.com/CorentinTh/it-tools/issues/254)) ([422b6eb](https://github.com/CorentinTh/it-tools/commit/422b6eb05a2fb5e7eec816a6bd2d37b53e4a6bdc)) |  | ||||||
| * **new-tool:** an svg placeholder image generator ([129f74c](https://github.com/CorentinTh/it-tools/commit/129f74c371eaf09fdc3a19afb709cee40b7aaf7f)) |  | ||||||
| * **new-tool:** hmac generator ([1bc6380](https://github.com/CorentinTh/it-tools/commit/1bc6380c6fdd7a9b500422a54bc508ab5557eb46)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **base64-to-string:** prevent validation error ([8a9e788](https://github.com/CorentinTh/it-tools/commit/8a9e7888dec41364c8c17b1234adcdc0616612b0)) |  | ||||||
| * **bip39-generator:** typo in validation message ([7570ad9](https://github.com/CorentinTh/it-tools/commit/7570ad965602233f860b9e03177a5b9dacf1b034)) |  | ||||||
| * **eta-calculator:** clamp inputs ([#249](https://github.com/CorentinTh/it-tools/issues/249)) ([531a25c](https://github.com/CorentinTh/it-tools/commit/531a25c1c4892835633ba5635c6ee48e1fbef31c)) |  | ||||||
| * **wording:** removed spaces before ponctuation ([#252](https://github.com/CorentinTh/it-tools/issues/252)) ([5f03619](https://github.com/CorentinTh/it-tools/commit/5f03619ab44c0b35455c46698ec37d79e87555b5)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **base64-to-file:** clean validation to convert base64 to file ([750a76b](https://github.com/CorentinTh/it-tools/commit/750a76b00fb79c0e9c2851c112141158ee0ffab1)) |  | ||||||
| * **display:** mutualized code display ([0be33fb](https://github.com/CorentinTh/it-tools/commit/0be33fb337e8d82474922c0fdf9555aa328cd729)) |  | ||||||
| * **lint:** externalization of prettier for simpler IDE support ([02c4963](https://github.com/CorentinTh/it-tools/commit/02c49635315661ca08deb0859c5ba33113368b9b)) |  | ||||||
| * **validation:** simplified validation system ([77b5b0c](https://github.com/CorentinTh/it-tools/commit/77b5b0cab50a05dcb419ce87d74517d82e7cd2c0)) |  | ||||||
|  |  | ||||||
| ### [2.9.2](https://github.com/CorentinTh/it-tools/compare/v2.9.1...v2.9.2) (2022-07-28) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **base64-file:** fixed url slug ([412de23](https://github.com/CorentinTh/it-tools/commit/412de23796babbc080b0768a75029ff2ddf2acfc)) |  | ||||||
| * **device-information:** handle of unknown values ([4f599b6](https://github.com/CorentinTh/it-tools/commit/4f599b699901a93444bcc67cbb3b3556a0561ae4)) |  | ||||||
| * **device-information:** prevent unwanted y-truncature of text  ([138149e](https://github.com/CorentinTh/it-tools/commit/138149e6f0be91255907a6083887898e5c68882e)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **base64-file:** fixed typo ([1a22d55](https://github.com/CorentinTh/it-tools/commit/1a22d55b3c48f58b05b5a50de4fea260e781fbef)) |  | ||||||
|  |  | ||||||
| ### [2.9.1](https://github.com/CorentinTh/it-tools/compare/v2.9.0...v2.9.1) (2022-07-25) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **base64:** split base64 text and file conversion in two tools + base64 to file ([e6953d1](https://github.com/CorentinTh/it-tools/commit/e6953d1b67b81a6d3c19973b706f29637c421f98)) |  | ||||||
|  |  | ||||||
| ## [2.9.0](https://github.com/CorentinTh/it-tools/compare/v2.8.0...v2.9.0) (2022-07-25) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| * **new-tool:** added a basic auth generator ([bdee93a](https://github.com/CorentinTh/it-tools/commit/bdee93a9e45c6b46e7f75cdcbe1907f138722dca)) |  | ||||||
|  |  | ||||||
| ## [2.8.0](https://github.com/CorentinTh/it-tools/compare/v2.7.0...v2.8.0) (2022-07-24) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| * **new-tool:** added an ETA calculator ([125a502](https://github.com/CorentinTh/it-tools/commit/125a50215a7abb9e0b59dbbc62aee49007b05ffe)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **sql-prettifier:** better responsiveness ([560fcf3](https://github.com/CorentinTh/it-tools/commit/560fcf3f783c66b9197e4a015420c43a729518bc)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **json-prettify:** improved layout for the json prettifier ([328fda6](https://github.com/CorentinTh/it-tools/commit/328fda65b3490869328467c5e2d5f538c689d9b6)) |  | ||||||
| * **sql-prettifier:** remove unused service files ([ba87097](https://github.com/CorentinTh/it-tools/commit/ba87097e3d834b6ea3212d28c2c33badb95f85e1)) |  | ||||||
|  |  | ||||||
| ## [2.7.0](https://github.com/CorentinTh/it-tools/compare/v2.6.0...v2.7.0) (2022-07-24) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| * **new-tool:** added an SQL prettifier and formatter ([d1f95f5](https://github.com/CorentinTh/it-tools/commit/d1f95f5b34a4570f1033a5289f0bd009d1aefb0c)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **typo:** fix few typos ([6cd25a7](https://github.com/CorentinTh/it-tools/commit/6cd25a743e32fceeaec8c1f8b94927a9c5d901f1)) |  | ||||||
|  |  | ||||||
| ## [2.6.0](https://github.com/CorentinTh/it-tools/compare/v2.5.3...v2.6.0) (2022-07-23) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| * **new-tool:** added chronometer ([130031c](https://github.com/CorentinTh/it-tools/commit/130031c2256f3d4d46948974b9de85ee6e92bf8b)) |  | ||||||
| * **search:** focus the search bar using Ctrl+K ([ab53048](https://github.com/CorentinTh/it-tools/commit/ab53048d5f6fdca7d00edbb79dee1a5409e6b11e)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **deps:** run dependencie audit auto fix ([a16161c](https://github.com/CorentinTh/it-tools/commit/a16161cdb48c064882b9dc91ec3d091d286f5c63)) |  | ||||||
| * **lint:** cleanned index.html ([c3a302b](https://github.com/CorentinTh/it-tools/commit/c3a302bc389a0e13aef4b14d5a9d3ec3a0d32729)) |  | ||||||
| * **text-statistics:** empty text mean 0 words and 0 lines ([92ce419](https://github.com/CorentinTh/it-tools/commit/92ce419f45e110509ab202485a36bf175ce345da)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * added accessibility labels on icon buttons ([394d085](https://github.com/CorentinTh/it-tools/commit/394d085846d976219ea775c21cd7e77f0f72a12b)) |  | ||||||
| * **import:** auto reordered imports ([2140842](https://github.com/CorentinTh/it-tools/commit/214084262cec7fb881fd397626356b080ea1a5cc)) |  | ||||||
|  |  | ||||||
| ### [2.5.3](https://github.com/CorentinTh/it-tools/compare/v2.5.2...v2.5.3) (2022-07-21) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * updated license in README ([e371e8f](https://github.com/CorentinTh/it-tools/commit/e371e8fedfd68f3cf6ecd3fbc9e2da8849f7d5bd)) |  | ||||||
|  |  | ||||||
| ### [2.5.2](https://github.com/CorentinTh/it-tools/compare/v2.5.1...v2.5.2) (2022-07-21) |  | ||||||
|  |  | ||||||
| ### [2.5.1](https://github.com/CorentinTh/it-tools/compare/v2.5.0...v2.5.1) (2022-06-01) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **lint:** missing dangling comma ([f05c8e1](https://github.com/CorentinTh/it-tools/commit/f05c8e1dc69275e529f4c8771ad55ba211e7fb5e)) |  | ||||||
| * menu label key value was undefined ([f48cd05](https://github.com/CorentinTh/it-tools/commit/f48cd058cf3381f3bc92ea8fe37b565327707d1e)) |  | ||||||
| * **title:** trully reactive tool title ([c2e1d59](https://github.com/CorentinTh/it-tools/commit/c2e1d59cb9d8dbb1bb072a46100192cb8c59f59b)) |  | ||||||
| * tool sorting inconsistencies in home page ([5ab4dd3](https://github.com/CorentinTh/it-tools/commit/5ab4dd3d4a42c3609d72597c7ba91764170e6e96)) |  | ||||||
|  |  | ||||||
| ## [2.5.0](https://github.com/CorentinTh/it-tools/compare/v2.4.2...v2.5.0) (2022-06-01) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| * **new-tool:** math evaluator ([433ba2a](https://github.com/CorentinTh/it-tools/commit/433ba2a3e5419eed0c96304b37693082224a1c73)) |  | ||||||
| * **tools:** new badge for recently created tools ([11720e6](https://github.com/CorentinTh/it-tools/commit/11720e6cdefc1da4bdd638415813b609840f8462)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **config:** updated env values loading ([2f61c74](https://github.com/CorentinTh/it-tools/commit/2f61c745f57962cf3bb9e2c1db4a3176df042808)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * removed unused import ([8fb0e6a](https://github.com/CorentinTh/it-tools/commit/8fb0e6af9c3be708d3f1777a1661e1b38f197a3f)) |  | ||||||
| * renammed Tool.ts to tool.ts ([ac89490](https://github.com/CorentinTh/it-tools/commit/ac89490794ee3c1c033859ffea31a962a13cc96d)) |  | ||||||
|  |  | ||||||
| ### [2.4.2](https://github.com/CorentinTh/it-tools/compare/v2.4.1...v2.4.2) (2022-06-01) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **config:** added config management with figue ([6becdbb](https://github.com/CorentinTh/it-tools/commit/6becdbb42329e1bdecf158707e37ba9f13ba1d2c)) |  | ||||||
| * **imports:** removed useless defineProps import ([5ce1262](https://github.com/CorentinTh/it-tools/commit/5ce1262fb44864b829dac09d5c0b9b68d522ceb7)) |  | ||||||
| * set coerent head title for home page ([a46d125](https://github.com/CorentinTh/it-tools/commit/a46d125c19902c2f41f37c62c07bb7b548d9f6f0)) |  | ||||||
|  |  | ||||||
| ### [2.4.1](https://github.com/CorentinTh/it-tools/compare/v2.4.0...v2.4.1) (2022-05-15) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **seo:** wrong url in share metas ([a88e4a9](https://github.com/CorentinTh/it-tools/commit/a88e4a9289e7d8cc80190f60f2fe08fe2ba08ee6)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **json-viewer:** add clear button ([048bc4a](https://github.com/CorentinTh/it-tools/commit/048bc4ae943509dea2946764efaa69f845b6c478)) |  | ||||||
| * **seo:** changed title string ([d4ea393](https://github.com/CorentinTh/it-tools/commit/d4ea393c1df87ae958a06ed66a11e36b081282d4)) |  | ||||||
|  |  | ||||||
| ## [2.4.0](https://github.com/CorentinTh/it-tools/compare/v2.3.2...v2.4.0) (2022-05-14) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| * catch throw on validation ([a60f64f](https://github.com/CorentinTh/it-tools/commit/a60f64f74417f811204121f97c16cdb4754afc3b)) |  | ||||||
| * **hash-text:** compute all hashes at the same time ([#242](https://github.com/CorentinTh/it-tools/issues/242)) ([e9cc499](https://github.com/CorentinTh/it-tools/commit/e9cc499ed87ba926086323223c7eca4f6658b3f0)) |  | ||||||
| * **new-tool:**  json viewer ([d356b14](https://github.com/CorentinTh/it-tools/commit/d356b1488fc640a4f5b65d62e0f2f368f5941996)) |  | ||||||
| * **seo:** added cannonical meta ([34bc6a5](https://github.com/CorentinTh/it-tools/commit/34bc6a57a7bab98ff2a630d02034c342084e0af9)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **lint:** missing new lines ([3cfc5f8](https://github.com/CorentinTh/it-tools/commit/3cfc5f8bc27b66e6fbb6054f3c909818083ebc37)) |  | ||||||
| * update recommended extension ids ([#244](https://github.com/CorentinTh/it-tools/issues/244)) ([1d7032d](https://github.com/CorentinTh/it-tools/commit/1d7032d0268220f594de6d837a303fc1e63cbd9f)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Documentation | ### Documentation | ||||||
|  | - **ide**: updated vscode extensions settings (#472) (847323c) | ||||||
|  |  | ||||||
| * added producthunt banners ([4c4da16](https://github.com/CorentinTh/it-tools/commit/4c4da16970e1dbb13705d8b6c020cd40cd2b5e0d)) | ### Chors | ||||||
|  | - **deps**: updated vueuse dependency version (8515c24) | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **base-layout:** renammed one letter variable ([383d975](https://github.com/CorentinTh/it-tools/commit/383d97569580c4f31448c07cb97e3778bc97a8af)) |  | ||||||
| * **date-converter:** mutualised and dry-ed code ([d2c767f](https://github.com/CorentinTh/it-tools/commit/d2c767f0922e9b93172c3167226ad3db5499b9f6)) |  | ||||||
| * **seo:** changed title string ([c3b6132](https://github.com/CorentinTh/it-tools/commit/c3b6132c261bd5952bafb1ff1e576eb13d2d0a7d)) |  | ||||||
| * updated description ([b89db3c](https://github.com/CorentinTh/it-tools/commit/b89db3c8d0de601fecbd2f9f79492dff1b461bd8)) |  | ||||||
|  |  | ||||||
| ### [2.3.2](https://github.com/CorentinTh/it-tools/compare/v2.3.1...v2.3.2) (2022-05-09) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **base-converter:** responsive input ([0b0cbd5](https://github.com/CorentinTh/it-tools/commit/0b0cbd55c3809ded2eedfa0b2238bc950b01516a)) |  | ||||||
| * **base64-converter:** async onUpload callback ([84cf1bb](https://github.com/CorentinTh/it-tools/commit/84cf1bb9645c5ae31579098df59471f7d99f6f0c)) |  | ||||||
| * **typo:** misspelings ([9755e51](https://github.com/CorentinTh/it-tools/commit/9755e51fe216e5e25c56417152e70cb5bce26b11)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **responsive:** row layout for multicards on big screens ([e21230b](https://github.com/CorentinTh/it-tools/commit/e21230bbd9550ba3315607b021a60a4f9f9e1b61)) |  | ||||||
|  |  | ||||||
| ### [2.3.1](https://github.com/CorentinTh/it-tools/compare/v2.3.0...v2.3.1) (2022-04-24) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * changed twitter account handler ([608ec3a](https://github.com/CorentinTh/it-tools/commit/608ec3a81db6583c8a2bf126b3868afd043c6981)) |  | ||||||
|  |  | ||||||
| ## [2.3.0](https://github.com/CorentinTh/it-tools/compare/v2.2.0...v2.3.0) (2022-04-22) |  | ||||||
|  |  | ||||||
|  | ## Version 2023.05.14-77f2efc | ||||||
|  |  | ||||||
| ### Features | ### 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) | ||||||
|  |  | ||||||
| * **new-tool:** html entities escape/unescape ([8e29a97](https://github.com/CorentinTh/it-tools/commit/8e29a97404ea0aa9b9b576656358c8c276b6f992)) | ### 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) | ||||||
|  |  | ||||||
| ### Bug Fixes | ### Chores | ||||||
|  | - **issues**: updated new tool request issue template (edae4c6) | ||||||
|  |  | ||||||
| * **head:** added titles for non-tool pages ([0a15892](https://github.com/CorentinTh/it-tools/commit/0a15892dde9852ff158a8fcb72d0ad6bae8bad02)) | ### Ui-lib | ||||||
| * **sider:** default collapsed value ([b22aa94](https://github.com/CorentinTh/it-tools/commit/b22aa941f52009118d4d3cc98277cc4c402a4c77)) | - **new-component**: added text input component in the c-lib (aad8d84) | ||||||
| * **sider:** missing href for link in footer ([c4dabcc](https://github.com/CorentinTh/it-tools/commit/c4dabccdaeac9d03163ac2588599b000e4e74562)) | - **button**: size variants (401f13f) | ||||||
| * **style:** hard width for group labels ([ebf6695](https://github.com/CorentinTh/it-tools/commit/ebf6695d2533db6f37b24dc7d338f422c551c8cb)) |  | ||||||
| * **url-parser:** cleaned weird margins on dark mode ([005ebfb](https://github.com/CorentinTh/it-tools/commit/005ebfba318ece1a9c04aefb737baed5d7aafb91)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **lint:** linter auto fix ([086d31e](https://github.com/CorentinTh/it-tools/commit/086d31eab5b3b1a927803eab5e650585f61abe19)) |  | ||||||
| * removed useless ref and value ([b12cbe4](https://github.com/CorentinTh/it-tools/commit/b12cbe412407389186a58e4ceaa94f5b441c11ea)) |  | ||||||
|  |  | ||||||
| ### [2.2.1](https://github.com/CorentinTh/it-tools/compare/v2.2.0...v2.2.1) (2022-04-21) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **head:** added titles for non-tool pages ([0a15892](https://github.com/CorentinTh/it-tools/commit/0a15892dde9852ff158a8fcb72d0ad6bae8bad02)) |  | ||||||
| * **sider:** missing href for link in footer ([c4dabcc](https://github.com/CorentinTh/it-tools/commit/c4dabccdaeac9d03163ac2588599b000e4e74562)) |  | ||||||
| * **style:** hard width for group labels ([ebf6695](https://github.com/CorentinTh/it-tools/commit/ebf6695d2533db6f37b24dc7d338f422c551c8cb)) |  | ||||||
| * **url-parser:** cleaned weird margins on dark mode ([005ebfb](https://github.com/CorentinTh/it-tools/commit/005ebfba318ece1a9c04aefb737baed5d7aafb91)) |  | ||||||
|  |  | ||||||
| ## [2.2.0](https://github.com/CorentinTh/it-tools/compare/v2.1.0...v2.2.0) (2022-04-18) |  | ||||||
|  |  | ||||||
|  | ## Version 2023.04.23-92bd835 | ||||||
|  |  | ||||||
| ### Features | ### 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) | ||||||
|  |  | ||||||
| * **new-tool:** url parser ([2b38d6f](https://github.com/CorentinTh/it-tools/commit/2b38d6f81e34845f896b858513e35209cba29f98)) | ### 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) | ||||||
|  |  | ||||||
| ### Bug Fixes | ## Version 2023.04.14-dbad773 | ||||||
|  |  | ||||||
| * **sider-footer:** fixed commit sha url ([ed9046d](https://github.com/CorentinTh/it-tools/commit/ed9046d3e1f5a7dc01c722ed139a2ae477a2d48f)) |  | ||||||
|  |  | ||||||
| ## [2.1.0](https://github.com/CorentinTh/it-tools/compare/v2.0.2...v2.1.0) (2022-04-18) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features | ### Features | ||||||
|  | - **new-tool**: http status codes (8355bd2) | ||||||
|  |  | ||||||
| * **new-tool:** bcrypt ([6d5856f](https://github.com/CorentinTh/it-tools/commit/6d5856fa93d1ffbf71856c75adc24ad87dc4b49b)) | ### Refactoring | ||||||
| * **new-tool:** device information ([277bd5f](https://github.com/CorentinTh/it-tools/commit/277bd5f0da359fd54c5164b376007d182a9fabde)) | - **uuid-generator**: prevent NaN in quantity (6fb4994) | ||||||
|  |  | ||||||
|  | ### Chores | ||||||
|  | - **release**: create a github release on new version (dbad773) | ||||||
|  | - **version**: reset CHANGELOG content to support new format (85cb0ff) | ||||||
|  |  | ||||||
| ### Refactors | ## Version 2023.04.14-f9b77b7 | ||||||
|  |  | ||||||
| * **menu:** removed burger menu icon tooltip ([09abffb](https://github.com/CorentinTh/it-tools/commit/09abffbcf9b09cb5adc34f8754b019d0c8b60854)) |  | ||||||
|  |  | ||||||
| ### [2.0.2](https://github.com/CorentinTh/it-tools/compare/v2.0.1...v2.0.2) (2022-04-18) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bug Fixes |  | ||||||
|  |  | ||||||
| * **git-memo:** pre scroll on overflow ([4fc303e](https://github.com/CorentinTh/it-tools/commit/4fc303e5e3f0bef9201cc002963e244a5d3be7b5)) |  | ||||||
| * **menu:** menu auto closed on mobile ([71f79a5](https://github.com/CorentinTh/it-tools/commit/71f79a5bbfe0dd5451a435c0a55e8b77ee7d3848)) |  | ||||||
| * **qr-code:** responsive layout ([cbf0b3d](https://github.com/CorentinTh/it-tools/commit/cbf0b3d6995e47d371a8fbcfccd65ba304fb08dc)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * **crontab:** list instead of table on small screen ([6b11de2](https://github.com/CorentinTh/it-tools/commit/6b11de258a8039fe7729130ede35d47592be7cbe)) |  | ||||||
| * removed empty sources ([a14cac6](https://github.com/CorentinTh/it-tools/commit/a14cac6d5c5967a47ca76a1d1a420115114c3bbf)) |  | ||||||
| * throw an error object instead of string ([4112fa5](https://github.com/CorentinTh/it-tools/commit/4112fa532e3d4be190d52bf3b11e0d4c3625a402)) |  | ||||||
|  |  | ||||||
| ### [2.0.1](https://github.com/CorentinTh/it-tools/compare/v2.0.0...v2.0.1) (2022-04-16) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features | ### Features | ||||||
|  | - **new-tool**: http status codes (8355bd2) | ||||||
|  |  | ||||||
| * **config:** added vercel.json ([2e046ad](https://github.com/CorentinTh/it-tools/commit/2e046ad09fed4a55bbf4449e3683a4150839c461)) | ### Refactoring | ||||||
|  | - **uuid-generator**: prevent NaN in quantity (6fb4994) | ||||||
|  |  | ||||||
|  | ### Chores | ||||||
|  | - **release**: create a github release on new version (f9b77b7) | ||||||
|  | - **version**: reset CHANGELOG content to support new format (85cb0ff) | ||||||
|  |  | ||||||
| ### Bug Fixes | ## Version 2023.04.14-2f0d239 | ||||||
|  |  | ||||||
| * remove duplicate property ([d066319](https://github.com/CorentinTh/it-tools/commit/d066319b45dee35df0212c7ff407013bd7449ae3)) |  | ||||||
| * **style:** url encode/decode layout ([34480b4](https://github.com/CorentinTh/it-tools/commit/34480b4e25ffc33536b03a0ba711c480219ad553)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Documentation |  | ||||||
|  |  | ||||||
| * updated description ([70a3df0](https://github.com/CorentinTh/it-tools/commit/70a3df044ea86ac35c1839ac5ab624f694fdd845)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Refactors |  | ||||||
|  |  | ||||||
| * clean imports ([724e142](https://github.com/CorentinTh/it-tools/commit/724e142222202798ea3df7d0fb34da1e7a5216a1)) |  | ||||||
| * lint fix ([a58ae24](https://github.com/CorentinTh/it-tools/commit/a58ae24d9409728ac12fb780f2c64643087de5be)) |  | ||||||
| * ref name ([5828085](https://github.com/CorentinTh/it-tools/commit/582808597c6aadf0feb48f6aae0a29b839e0dd54)) |  | ||||||
|  |  | ||||||
| ## 2.0.0 (2022-04-16) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Features | ### Features | ||||||
|  | - **new-tool**: http status codes (8355bd2) | ||||||
|  |  | ||||||
| * **a11y:** aria-label on icon button ([5f50275](https://github.com/CorentinTh/it-tools/commit/5f502755d69ab21a78d9256db8a1c64f1ab82c2a)) | ### Refactoring | ||||||
| * added commit short sha ([668625c](https://github.com/CorentinTh/it-tools/commit/668625c6dab6e8b98f363df6c0aa3bf00a3afaa4)) | - **uuid-generator**: prevent NaN in quantity (6fb4994) | ||||||
| * added plausible tracker ([0808920](https://github.com/CorentinTh/it-tools/commit/0808920951b55c938537f33353a37ece96b04084)) |  | ||||||
| * added twitter link ([d126abc](https://github.com/CorentinTh/it-tools/commit/d126abc7b12a9fce778fe9883e44dca581509778)) |  | ||||||
| * footer in sider ([3f03850](https://github.com/CorentinTh/it-tools/commit/3f038503dd705ba3a5562a1e8f85a3b0e7d0be5b)) |  | ||||||
| * **layout:** menu category ([9c9be9e](https://github.com/CorentinTh/it-tools/commit/9c9be9e2e2e2c856d1af1df9d9d37a64460cd82b)) |  | ||||||
| * mobile friendly menu ([1e67fa6](https://github.com/CorentinTh/it-tools/commit/1e67fa6e0bede8c055d9e4cb9bf7f97423bc9bdf)) |  | ||||||
| * **navbar:** added github link ([d4e226e](https://github.com/CorentinTh/it-tools/commit/d4e226e09face78da794fa7e676eef85d05dde75)) |  | ||||||
| * **nav:** navigation tooltips ([b892f50](https://github.com/CorentinTh/it-tools/commit/b892f50cd633d42e6261be208bd077d92d336afb)) |  | ||||||
| * **page:** added 404 page ([3db4f91](https://github.com/CorentinTh/it-tools/commit/3db4f91c27a2ab37bb23d8feb77b6dffa9a92977)) |  | ||||||
| * **page:** home page layout ([57fd14a](https://github.com/CorentinTh/it-tools/commit/57fd14a199a253f49f3c53810490e5d31512b261)) |  | ||||||
| * persistent theme selection fallback to prefered theme ([40e9af0](https://github.com/CorentinTh/it-tools/commit/40e9af06cf28b7348152f8ec3898fa2b27ec0b21)) |  | ||||||
| * **router:** added legacy routes redirections ([dbce46b](https://github.com/CorentinTh/it-tools/commit/dbce46b470b0187a395cdd350a023641c6319582)) |  | ||||||
| * search-bar ([e8594de](https://github.com/CorentinTh/it-tools/commit/e8594de7b45102b8bc1cfb82d0839e3722d9c4c2)) |  | ||||||
| * **search:** round and clearable searchbar ([b112f5f](https://github.com/CorentinTh/it-tools/commit/b112f5f226c6b03151bbeb4fc607e449c444e667)) |  | ||||||
| * **seo:** added robots.txt and humans.txt ([cd9a3bc](https://github.com/CorentinTh/it-tools/commit/cd9a3bc9b10cf7363301e9a0d0b17f38ea640e0c)) |  | ||||||
| * **seo:** added title + description ([5f74037](https://github.com/CorentinTh/it-tools/commit/5f74037105c5e8efc5bdad2261597458cfcf26d3)) |  | ||||||
| * **seo:** pwa and icons ([b7193e8](https://github.com/CorentinTh/it-tools/commit/b7193e838ba83d0548211cff922e107a1f11f90f)) |  | ||||||
| * **share:** social image ([39746e0](https://github.com/CorentinTh/it-tools/commit/39746e07c53c22ac132ad2aaf25dd71bb6458cde)) |  | ||||||
| * **style:** dark mode ([3e92b7f](https://github.com/CorentinTh/it-tools/commit/3e92b7f1e04a709df231fce22801b55619e8faab)) |  | ||||||
| * **style:** theme overrides ([d542688](https://github.com/CorentinTh/it-tools/commit/d542688664cc9c675d1d26f4278a25f1b9e3f28d)) |  | ||||||
| * **tool:** add lch in color converter ([b5243c4](https://github.com/CorentinTh/it-tools/commit/b5243c43638f37a2d727b015bba61fab0d1b9fe9)) |  | ||||||
| * **tool:** added token generator ([40dec52](https://github.com/CorentinTh/it-tools/commit/40dec52c8467fd27eb8f3857ed72746ebaa4f509)) |  | ||||||
| * **tool:** base converter ([034c686](https://github.com/CorentinTh/it-tools/commit/034c686896d0443ea587cd152535b2227234c011)) |  | ||||||
| * **tool:** base64 string converter ([203b6a9](https://github.com/CorentinTh/it-tools/commit/203b6a9d73dcb30182b130de59920534e18b76b4)) |  | ||||||
| * **tool:** bip39-generator ([d55329f](https://github.com/CorentinTh/it-tools/commit/d55329f3abc3d3f8ad48def7d7f63b44cd768e27)) |  | ||||||
| * **tool:** bip39-generator ([765c010](https://github.com/CorentinTh/it-tools/commit/765c010700c07b2809daef0e7c694ac265ce9ddc)) |  | ||||||
| * **tool:** case converter ([7a7372d](https://github.com/CorentinTh/it-tools/commit/7a7372df191abc7ecd3fee7234d4de7aaaba03f6)) |  | ||||||
| * **tool:** color converter ([4e50b7a](https://github.com/CorentinTh/it-tools/commit/4e50b7a973e950819a52c127db2a754838cbbf8e)) |  | ||||||
| * **tool:** crontab generator ([358ff45](https://github.com/CorentinTh/it-tools/commit/358ff45ae1d9822b8a7c342515f668d25b7128b5)) |  | ||||||
| * **tool:** date-time converter ([2d9cb20](https://github.com/CorentinTh/it-tools/commit/2d9cb209b377326f4bf62067db7d5ad0c7eb7bde)) |  | ||||||
| * **tool:** encryption ([888ab2c](https://github.com/CorentinTh/it-tools/commit/888ab2cf378597e2880b6dd6a013f3bc192f2b1a)) |  | ||||||
| * **tool:** git memo ([5cd9997](https://github.com/CorentinTh/it-tools/commit/5cd9997a845f6d5f82d3ae74d3ec12603224517d)) |  | ||||||
| * **tool:** lorem ipsum generator ([5dcb2ed](https://github.com/CorentinTh/it-tools/commit/5dcb2ed95c318ea1c4134da207c844672d0fbbd8)) |  | ||||||
| * **tool:** qr-code generator ([5582d75](https://github.com/CorentinTh/it-tools/commit/5582d75927b560d9259929c787c0809634d1f8ae)) |  | ||||||
| * **tool:** random port generator ([7c540f1](https://github.com/CorentinTh/it-tools/commit/7c540f1208da749c3932aab8f2c392048c4546ae)) |  | ||||||
| * **tool:** roman-arabic numbers converter ([655019c](https://github.com/CorentinTh/it-tools/commit/655019cf23babcec2a2f1e03cac87744e3139304)) |  | ||||||
| * **tool:** text hash ([0f3b744](https://github.com/CorentinTh/it-tools/commit/0f3b7445ad1f945d9b364476147bf824ac309a6c)) |  | ||||||
| * **tool:** text statistics ([0a7c325](https://github.com/CorentinTh/it-tools/commit/0a7c3252e36a4769eedaaec4524b4ee2ae2b19c7)) |  | ||||||
| * **tool:** url encode/decode ([afac566](https://github.com/CorentinTh/it-tools/commit/afac5664c802c8480fe2c457bcfb7f5e26829cdf)) |  | ||||||
| * **tool:** uuid v4 generator ([3ae6114](https://github.com/CorentinTh/it-tools/commit/3ae61147a94791987e9e326b19063579976d8dc0)) |  | ||||||
| * **ux:** copyable input ([1859a9a](https://github.com/CorentinTh/it-tools/commit/1859a9a174010789dcd7ecefb2451e1de7b60b4c)) |  | ||||||
|  |  | ||||||
|  | ### Chores | ||||||
|  | - **release**: create a github release on new version (2f0d239) | ||||||
|  | - **version**: reset CHANGELOG content to support new format (85cb0ff) | ||||||
|  |  | ||||||
| ### Bug Fixes | ## Version 2023.04.14-474cae4 | ||||||
|  |  | ||||||
| * **hash-text:** added missing toString() ([4ca5fce](https://github.com/CorentinTh/it-tools/commit/4ca5fce911c3312d56bca1ffba863b2f37841c9e)) | ### Features | ||||||
| * **hash-text:** correct copy message ([bab92ef](https://github.com/CorentinTh/it-tools/commit/bab92ef84f66372df40ce385c2949518ed158427)) | - **new-tool**: http status codes (8355bd2) | ||||||
| * removed global define ([889d594](https://github.com/CorentinTh/it-tools/commit/889d59499212a449ee460c68c480648e337a7ecb)) |  | ||||||
| * **style:** working dark mode persistence ([3ae8728](https://github.com/CorentinTh/it-tools/commit/3ae872847b00d65e4e2e629775d479a3333450f1)) |  | ||||||
| * **validation:** proper rules ([11d8110](https://github.com/CorentinTh/it-tools/commit/11d8110226e22e30ae16d297628c1d252a93be9e)) |  | ||||||
|  |  | ||||||
|  | ### Refactoring | ||||||
|  | - **uuid-generator**: prevent NaN in quantity (6fb4994) | ||||||
|  |  | ||||||
| ### Refactors | ### Chores | ||||||
|  | - **release**: create a github release on new version (474cae4) | ||||||
|  | - **version**: reset CHANGELOG content to support new format (85cb0ff) | ||||||
|  |  | ||||||
| * better icon ([0af7d81](https://github.com/CorentinTh/it-tools/commit/0af7d81abd987aa5d1b0321c25a65131d978e929)) | ## Version v2023.4.13-dce9ff9 | ||||||
| * **clean:** removed extra console.log ([82606f6](https://github.com/CorentinTh/it-tools/commit/82606f6a477fce2041ab33adc7e95bcba4343e2b)) |  | ||||||
| * embeded sider scrollbar ([f872972](https://github.com/CorentinTh/it-tools/commit/f872972e69aeb4fde4c17f0c122ca3fd4aa1c56c)) |  | ||||||
| * icon sizes ([9bb7fc4](https://github.com/CorentinTh/it-tools/commit/9bb7fc47aa70bdc5083d0883f1496fac63f812ea)) |  | ||||||
| * menu option key ([390ef93](https://github.com/CorentinTh/it-tools/commit/390ef93232dc1b448022a0c09d36367adad9d221)) |  | ||||||
| * **page:** removed unused import ([f70fce6](https://github.com/CorentinTh/it-tools/commit/f70fce65e20989eb19b0f0976e756a43edf02e9d)) |  | ||||||
| * removed theme editor ([8559fbd](https://github.com/CorentinTh/it-tools/commit/8559fbd7744fe82b7702a5c0eb77a8d627c5a73d)) |  | ||||||
| * removed unused files ([c1e7669](https://github.com/CorentinTh/it-tools/commit/c1e76695e4a16b8312ab6031a1bdfb6368946677)) |  | ||||||
| * removed unused files ([8d9f924](https://github.com/CorentinTh/it-tools/commit/8d9f92417744a5fbd9b4108e851005f23de18b53)) |  | ||||||
| * **style:** cleaner layout ([1d09a01](https://github.com/CorentinTh/it-tools/commit/1d09a01bb25088493cc9b7f2cb7f8a8aa69ac9e9)) |  | ||||||
| * **style:** improve style for tool-card ([65a6896](https://github.com/CorentinTh/it-tools/commit/65a6896563d16f30420424e274bd306e3e9182c8)) |  | ||||||
| * **style:** label width ([fd4426d](https://github.com/CorentinTh/it-tools/commit/fd4426d246ada553528759f761c8192df85c0d44)) |  | ||||||
| * **style:** menu item height ([8951e87](https://github.com/CorentinTh/it-tools/commit/8951e87c143fda74be32bae5b28e009556d7086e)) |  | ||||||
| * **style:** menu scrollbar ([483cf66](https://github.com/CorentinTh/it-tools/commit/483cf66db992169d361487c8461938810793b978)) |  | ||||||
| * **style:** port display ([2632f24](https://github.com/CorentinTh/it-tools/commit/2632f24cc89af7dd12f7a0c1a8b58983a1bb78d8)) |  | ||||||
| * **style:** removed extra br ([b44539c](https://github.com/CorentinTh/it-tools/commit/b44539c1820defbaaa6dfe83a76c72982a641971)) |  | ||||||
| * **style:** replaced scss style block to less ([655d9d2](https://github.com/CorentinTh/it-tools/commit/655d9d22e3136bdf1dee29310ab04cf38596bdc8)) |  | ||||||
| * **style:** responsive layout ([2df3f53](https://github.com/CorentinTh/it-tools/commit/2df3f53b78bbe419763fd359788a4b0b5710e4b7)) |  | ||||||
| * **style:** updated linter config ([6b58ec5](https://github.com/CorentinTh/it-tools/commit/6b58ec554a0de91139f16d67cec42536d093d5fb)) |  | ||||||
|  |  | ||||||
|  | _Diff not available_ | ||||||
| ### Documentation |  | ||||||
|  |  | ||||||
| * added new tool creation procedure ([8177883](https://github.com/CorentinTh/it-tools/commit/81778834e6a79725c42eae1772935682ce7580c6)) |  | ||||||
| * updated readme ([1134e0b](https://github.com/CorentinTh/it-tools/commit/1134e0b822edbc25ce9ff83007bf5d331a1becbd)) |  | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								README.md
									
									
									
									
									
								
							| @@ -6,7 +6,7 @@ Useful tools for developer and people working in IT. [Have a look !](https://it- | |||||||
|  |  | ||||||
| Please check the [issues](https://github.com/CorentinTh/it-tools/issues) to see if some feature listed to be implemented. | Please check the [issues](https://github.com/CorentinTh/it-tools/issues) to see if some feature listed to be implemented. | ||||||
|  |  | ||||||
| You have an idea of a tool? Submit a [feature request](https://github.com/CorentinTh/it-tools/issues/new?assignees=corentinth&labels=&template=feature_request.md&title=)! | You have an idea of a tool? Submit a [feature request](https://github.com/CorentinTh/it-tools/issues/new/choose)! | ||||||
|  |  | ||||||
| ## Self host | ## Self host | ||||||
|  |  | ||||||
| @@ -26,6 +26,7 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent | |||||||
|  |  | ||||||
| **Other solutions:** | **Other solutions:** | ||||||
|  |  | ||||||
|  | - [Cloudron](https://www.cloudron.io/store/tech.ittools.cloudron.html) | ||||||
| - [Tipi](https://www.runtipi.io/docs/apps-available) | - [Tipi](https://www.runtipi.io/docs/apps-available) | ||||||
| - [Unraid](https://unraid.net/community/apps?q=it-tools) | - [Unraid](https://unraid.net/community/apps?q=it-tools) | ||||||
|  |  | ||||||
| @@ -33,7 +34,25 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent | |||||||
|  |  | ||||||
| ### Recommended IDE Setup | ### Recommended IDE Setup | ||||||
|  |  | ||||||
| [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). | [VSCode](https://code.visualstudio.com/) with the following extensions: | ||||||
|  |  | ||||||
|  | - [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) | ||||||
|  | - [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). | ||||||
|  | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) | ||||||
|  | - [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) | ||||||
|  |  | ||||||
|  | with the following settings: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "editor.formatOnSave": false, | ||||||
|  |   "editor.codeActionsOnSave": { | ||||||
|  |     "source.fixAll.eslint": true | ||||||
|  |   }, | ||||||
|  |   "i18n-ally.localesPaths": ["locales", "src/tools/*/locales"], | ||||||
|  |   "i18n-ally.keystyle": "nested" | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ### Type Support for `.vue` Imports in TS | ### Type Support for `.vue` Imports in TS | ||||||
|  |  | ||||||
| @@ -84,7 +103,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 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										354
									
								
								auto-imports.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										354
									
								
								auto-imports.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -19,7 +19,9 @@ declare global { | |||||||
|   const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] |   const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] | ||||||
|   const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] |   const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] | ||||||
|   const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] |   const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] | ||||||
|  |   const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] | ||||||
|   const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] |   const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] | ||||||
|  |   const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] | ||||||
|   const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] |   const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] | ||||||
|   const customRef: typeof import('vue')['customRef'] |   const customRef: typeof import('vue')['customRef'] | ||||||
|   const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] |   const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] | ||||||
| @@ -39,9 +41,6 @@ declare global { | |||||||
|   const isReactive: typeof import('vue')['isReactive'] |   const isReactive: typeof import('vue')['isReactive'] | ||||||
|   const isReadonly: typeof import('vue')['isReadonly'] |   const isReadonly: typeof import('vue')['isReadonly'] | ||||||
|   const isRef: typeof import('vue')['isRef'] |   const isRef: typeof import('vue')['isRef'] | ||||||
|   const logicAnd: typeof import('@vueuse/core')['logicAnd'] |  | ||||||
|   const logicNot: typeof import('@vueuse/core')['logicNot'] |  | ||||||
|   const logicOr: typeof import('@vueuse/core')['logicOr'] |  | ||||||
|   const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] |   const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] | ||||||
|   const markRaw: typeof import('vue')['markRaw'] |   const markRaw: typeof import('vue')['markRaw'] | ||||||
|   const nextTick: typeof import('vue')['nextTick'] |   const nextTick: typeof import('vue')['nextTick'] | ||||||
| @@ -94,6 +93,7 @@ declare global { | |||||||
|   const toReactive: typeof import('@vueuse/core')['toReactive'] |   const toReactive: typeof import('@vueuse/core')['toReactive'] | ||||||
|   const toRef: typeof import('vue')['toRef'] |   const toRef: typeof import('vue')['toRef'] | ||||||
|   const toRefs: typeof import('vue')['toRefs'] |   const toRefs: typeof import('vue')['toRefs'] | ||||||
|  |   const toValue: typeof import('vue')['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'] | ||||||
| @@ -157,6 +170,7 @@ declare global { | |||||||
|   const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] |   const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] | ||||||
|   const useGamepad: typeof import('@vueuse/core')['useGamepad'] |   const useGamepad: typeof import('@vueuse/core')['useGamepad'] | ||||||
|   const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] |   const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] | ||||||
|  |   const useI18n: typeof import('vue-i18n')['useI18n'] | ||||||
|   const useIdle: typeof import('@vueuse/core')['useIdle'] |   const useIdle: typeof import('@vueuse/core')['useIdle'] | ||||||
|   const useImage: typeof import('@vueuse/core')['useImage'] |   const useImage: typeof import('@vueuse/core')['useImage'] | ||||||
|   const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] |   const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] | ||||||
| @@ -189,12 +203,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 +228,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 +250,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 +272,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 +309,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 +331,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']> | ||||||
| @@ -357,6 +383,7 @@ declare module 'vue' { | |||||||
|     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('vue')['toRef']> | ||||||
|     readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']> |     readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']> | ||||||
|  |     readonly toValue: UnwrapRef<typeof import('vue')['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 +394,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 +417,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']> | ||||||
| @@ -420,6 +460,7 @@ declare module 'vue' { | |||||||
|     readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']> |     readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']> | ||||||
|     readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']> |     readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']> | ||||||
|     readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']> |     readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']> | ||||||
|  |     readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']> | ||||||
|     readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']> |     readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']> | ||||||
|     readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']> |     readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']> | ||||||
|     readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']> |     readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']> | ||||||
| @@ -452,12 +493,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 +518,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 +540,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 +562,294 @@ 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 watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']> | ||||||
|  |     readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']> | ||||||
|  |     readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']> | ||||||
|  |     readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']> | ||||||
|  |     readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']> | ||||||
|  |     readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']> | ||||||
|  |     readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']> | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | declare module '@vue/runtime-core' { | ||||||
|  |   interface ComponentCustomProperties { | ||||||
|  |     readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']> | ||||||
|  |     readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']> | ||||||
|  |     readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']> | ||||||
|  |     readonly computed: UnwrapRef<typeof import('vue')['computed']> | ||||||
|  |     readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']> | ||||||
|  |     readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']> | ||||||
|  |     readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']> | ||||||
|  |     readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']> | ||||||
|  |     readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']> | ||||||
|  |     readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']> | ||||||
|  |     readonly createApp: UnwrapRef<typeof import('vue')['createApp']> | ||||||
|  |     readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']> | ||||||
|  |     readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']> | ||||||
|  |     readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']> | ||||||
|  |     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 createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']> | ||||||
|  |     readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']> | ||||||
|  |     readonly customRef: UnwrapRef<typeof import('vue')['customRef']> | ||||||
|  |     readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']> | ||||||
|  |     readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']> | ||||||
|  |     readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']> | ||||||
|  |     readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']> | ||||||
|  |     readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']> | ||||||
|  |     readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']> | ||||||
|  |     readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']> | ||||||
|  |     readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']> | ||||||
|  |     readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']> | ||||||
|  |     readonly h: UnwrapRef<typeof import('vue')['h']> | ||||||
|  |     readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']> | ||||||
|  |     readonly inject: UnwrapRef<typeof import('vue')['inject']> | ||||||
|  |     readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']> | ||||||
|  |     readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']> | ||||||
|  |     readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> | ||||||
|  |     readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']> | ||||||
|  |     readonly isRef: UnwrapRef<typeof import('vue')['isRef']> | ||||||
|  |     readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']> | ||||||
|  |     readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']> | ||||||
|  |     readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']> | ||||||
|  |     readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']> | ||||||
|  |     readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']> | ||||||
|  |     readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']> | ||||||
|  |     readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']> | ||||||
|  |     readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']> | ||||||
|  |     readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']> | ||||||
|  |     readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']> | ||||||
|  |     readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']> | ||||||
|  |     readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']> | ||||||
|  |     readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']> | ||||||
|  |     readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']> | ||||||
|  |     readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']> | ||||||
|  |     readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']> | ||||||
|  |     readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']> | ||||||
|  |     readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']> | ||||||
|  |     readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']> | ||||||
|  |     readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']> | ||||||
|  |     readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']> | ||||||
|  |     readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']> | ||||||
|  |     readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']> | ||||||
|  |     readonly provide: UnwrapRef<typeof import('vue')['provide']> | ||||||
|  |     readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']> | ||||||
|  |     readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']> | ||||||
|  |     readonly reactive: UnwrapRef<typeof import('vue')['reactive']> | ||||||
|  |     readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']> | ||||||
|  |     readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']> | ||||||
|  |     readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']> | ||||||
|  |     readonly readonly: UnwrapRef<typeof import('vue')['readonly']> | ||||||
|  |     readonly ref: UnwrapRef<typeof import('vue')['ref']> | ||||||
|  |     readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']> | ||||||
|  |     readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']> | ||||||
|  |     readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']> | ||||||
|  |     readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']> | ||||||
|  |     readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']> | ||||||
|  |     readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']> | ||||||
|  |     readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']> | ||||||
|  |     readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']> | ||||||
|  |     readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']> | ||||||
|  |     readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']> | ||||||
|  |     readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']> | ||||||
|  |     readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']> | ||||||
|  |     readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']> | ||||||
|  |     readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']> | ||||||
|  |     readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']> | ||||||
|  |     readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']> | ||||||
|  |     readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']> | ||||||
|  |     readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']> | ||||||
|  |     readonly toRef: UnwrapRef<typeof import('vue')['toRef']> | ||||||
|  |     readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']> | ||||||
|  |     readonly toValue: UnwrapRef<typeof import('vue')['toValue']> | ||||||
|  |     readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']> | ||||||
|  |     readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']> | ||||||
|  |     readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']> | ||||||
|  |     readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']> | ||||||
|  |     readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']> | ||||||
|  |     readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']> | ||||||
|  |     readonly unref: UnwrapRef<typeof import('vue')['unref']> | ||||||
|  |     readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']> | ||||||
|  |     readonly until: UnwrapRef<typeof import('@vueuse/core')['until']> | ||||||
|  |     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 useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']> | ||||||
|  |     readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']> | ||||||
|  |     readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']> | ||||||
|  |     readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']> | ||||||
|  |     readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']> | ||||||
|  |     readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']> | ||||||
|  |     readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']> | ||||||
|  |     readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']> | ||||||
|  |     readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']> | ||||||
|  |     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 useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']> | ||||||
|  |     readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']> | ||||||
|  |     readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']> | ||||||
|  |     readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']> | ||||||
|  |     readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']> | ||||||
|  |     readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']> | ||||||
|  |     readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']> | ||||||
|  |     readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']> | ||||||
|  |     readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']> | ||||||
|  |     readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']> | ||||||
|  |     readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']> | ||||||
|  |     readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']> | ||||||
|  |     readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']> | ||||||
|  |     readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']> | ||||||
|  |     readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']> | ||||||
|  |     readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']> | ||||||
|  |     readonly useDialog: UnwrapRef<typeof import('naive-ui')['useDialog']> | ||||||
|  |     readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']> | ||||||
|  |     readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']> | ||||||
|  |     readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']> | ||||||
|  |     readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']> | ||||||
|  |     readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']> | ||||||
|  |     readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']> | ||||||
|  |     readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']> | ||||||
|  |     readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']> | ||||||
|  |     readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']> | ||||||
|  |     readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']> | ||||||
|  |     readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']> | ||||||
|  |     readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']> | ||||||
|  |     readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']> | ||||||
|  |     readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']> | ||||||
|  |     readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']> | ||||||
|  |     readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']> | ||||||
|  |     readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']> | ||||||
|  |     readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']> | ||||||
|  |     readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']> | ||||||
|  |     readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']> | ||||||
|  |     readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']> | ||||||
|  |     readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']> | ||||||
|  |     readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']> | ||||||
|  |     readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']> | ||||||
|  |     readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']> | ||||||
|  |     readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']> | ||||||
|  |     readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']> | ||||||
|  |     readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']> | ||||||
|  |     readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']> | ||||||
|  |     readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']> | ||||||
|  |     readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']> | ||||||
|  |     readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']> | ||||||
|  |     readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']> | ||||||
|  |     readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']> | ||||||
|  |     readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']> | ||||||
|  |     readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']> | ||||||
|  |     readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']> | ||||||
|  |     readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']> | ||||||
|  |     readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']> | ||||||
|  |     readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']> | ||||||
|  |     readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']> | ||||||
|  |     readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']> | ||||||
|  |     readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']> | ||||||
|  |     readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']> | ||||||
|  |     readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']> | ||||||
|  |     readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']> | ||||||
|  |     readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']> | ||||||
|  |     readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']> | ||||||
|  |     readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']> | ||||||
|  |     readonly useNotification: UnwrapRef<typeof import('naive-ui')['useNotification']> | ||||||
|  |     readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']> | ||||||
|  |     readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']> | ||||||
|  |     readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']> | ||||||
|  |     readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']> | ||||||
|  |     readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']> | ||||||
|  |     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 usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']> | ||||||
|  |     readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']> | ||||||
|  |     readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']> | ||||||
|  |     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 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 useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']> | ||||||
|  |     readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']> | ||||||
|  |     readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']> | ||||||
|  |     readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']> | ||||||
|  |     readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']> | ||||||
|  |     readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']> | ||||||
|  |     readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']> | ||||||
|  |     readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']> | ||||||
|  |     readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']> | ||||||
|  |     readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']> | ||||||
|  |     readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']> | ||||||
|  |     readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']> | ||||||
|  |     readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']> | ||||||
|  |     readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']> | ||||||
|  |     readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']> | ||||||
|  |     readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']> | ||||||
|  |     readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']> | ||||||
|  |     readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']> | ||||||
|  |     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 useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']> | ||||||
|  |     readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']> | ||||||
|  |     readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']> | ||||||
|  |     readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']> | ||||||
|  |     readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']> | ||||||
|  |     readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']> | ||||||
|  |     readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']> | ||||||
|  |     readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']> | ||||||
|  |     readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']> | ||||||
|  |     readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']> | ||||||
|  |     readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']> | ||||||
|  |     readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']> | ||||||
|  |     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 useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']> | ||||||
|  |     readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']> | ||||||
|  |     readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']> | ||||||
|  |     readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']> | ||||||
|  |     readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']> | ||||||
|  |     readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']> | ||||||
|  |     readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']> | ||||||
|  |     readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']> | ||||||
|  |     readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']> | ||||||
|  |     readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']> | ||||||
|  |     readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']> | ||||||
|  |     readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']> | ||||||
|  |     readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']> | ||||||
|  |     readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']> | ||||||
|  |     readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']> | ||||||
|  |     readonly watch: UnwrapRef<typeof import('vue')['watch']> | ||||||
|  |     readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']> | ||||||
|  |     readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']> | ||||||
|  |     readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']> | ||||||
|  |     readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']> | ||||||
|  |     readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']> | ||||||
|  |     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']> | ||||||
|   | |||||||
							
								
								
									
										144
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										144
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -9,18 +9,117 @@ 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'] | ||||||
|  |     CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.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'] | ||||||
|  |     CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default'] | ||||||
|  |     CKeyValueListItem: typeof import('./src/ui/c-key-value-list/c-key-value-list-item.vue')['default'] | ||||||
|  |     CLabel: typeof import('./src/ui/c-label/c-label.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'] | ||||||
|  |     CModal: typeof import('./src/ui/c-modal/c-modal.vue')['default'] | ||||||
|  |     'CModal.demo': typeof import('./src/ui/c-modal/c-modal.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'] | ||||||
|  |     CommandPalette: typeof import('./src/modules/command-palette/command-palette.vue')['default'] | ||||||
|  |     CommandPaletteOption: typeof import('./src/modules/command-palette/components/command-palette-option.vue')['default'] | ||||||
|  |     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] | ||||||
|  |     CSelect: typeof import('./src/ui/c-select/c-select.vue')['default'] | ||||||
|  |     'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default'] | ||||||
|  |     CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default'] | ||||||
|  |     'CTextCopyable.demo': typeof import('./src/ui/c-text-copyable/c-text-copyable.demo.vue')['default'] | ||||||
|  |     CTooltip: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default'] | ||||||
|  |     'CTooltip.demo': typeof import('./src/ui/c-tooltip/c-tooltip.demo.vue')['default'] | ||||||
|  |     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] | ||||||
|  |     'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.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'] | ||||||
|  |     EmojiCard: typeof import('./src/tools/emoji-picker/emoji-card.vue')['default'] | ||||||
|  |     EmojiGrid: typeof import('./src/tools/emoji-picker/emoji-grid.vue')['default'] | ||||||
|  |     EmojiPicker: typeof import('./src/tools/emoji-picker/emoji-picker.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.vue')['default'] | ||||||
|  |     'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.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'] | ||||||
|  |     IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default'] | ||||||
|  |     'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] | ||||||
|  |     'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default'] | ||||||
|  |     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] | ||||||
|  |     IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] | ||||||
|  |     IconMdiCamera: typeof import('~icons/mdi/camera')['default'] | ||||||
|  |     IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] | ||||||
|  |     IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['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'] | ||||||
|  |     IconMdiSearch: typeof import('~icons/mdi/search')['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'] | ||||||
|  |     JsonToCsv: typeof import('./src/tools/json-to-csv/json-to-csv.vue')['default'] | ||||||
|  |     JsonToToml: typeof import('./src/tools/json-to-toml/json-to-toml.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'] |  | ||||||
|     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'] | ||||||
| @@ -39,7 +138,6 @@ declare module '@vue/runtime-core' { | |||||||
|     NH3: typeof import('naive-ui')['NH3'] |     NH3: typeof import('naive-ui')['NH3'] | ||||||
|     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'] |  | ||||||
|     NInputGroup: typeof import('naive-ui')['NInputGroup'] |     NInputGroup: typeof import('naive-ui')['NInputGroup'] | ||||||
|     NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] |     NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] | ||||||
|     NInputNumber: typeof import('naive-ui')['NInputNumber'] |     NInputNumber: typeof import('naive-ui')['NInputNumber'] | ||||||
| @@ -49,24 +147,50 @@ 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'] |  | ||||||
|     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'] | ||||||
|     NTag: typeof import('naive-ui')['NTag'] |     NTag: typeof import('naive-ui')['NTag'] | ||||||
|     NText: typeof import('naive-ui')['NText'] |  | ||||||
|     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'] | ||||||
|  |     PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] | ||||||
|  |     PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.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'] | ||||||
|     SearchBar: typeof import('./src/components/SearchBar.vue')['default'] |     RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.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'] | ||||||
|  |     StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.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'] | ||||||
|  |     TextDiff: typeof import('./src/tools/text-diff/text-diff.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'] | ||||||
|  |     TomlToJson: typeof import('./src/tools/toml-to-json/toml-to-json.vue')['default'] | ||||||
|  |     TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.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'] | ||||||
|  |     WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default'] | ||||||
|  |     XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default'] | ||||||
|  |     YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default'] | ||||||
|  |     YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default'] | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								index.html
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ | |||||||
| <html lang="en"> | <html lang="en"> | ||||||
|   <head> |   <head> | ||||||
|     <meta charset="UTF-8" /> |     <meta charset="UTF-8" /> | ||||||
|     <link rel="icon" href="/favicon.ico" /> |     <link rel="icon" href="favicon.ico" /> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|     <title>IT Tools - Handy online tools for developers</title> |     <title>IT Tools - Handy online tools for developers</title> | ||||||
|     <meta itemprop="name" content="IT Tools - Handy online tools for developers" /> |     <meta itemprop="name" content="IT Tools - Handy online tools for developers" /> | ||||||
| @@ -14,13 +14,13 @@ | |||||||
|       itemprop="description" |       itemprop="description" | ||||||
|       content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." |       content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." | ||||||
|     /> |     /> | ||||||
|     <link rel="author" href="/humans.txt" /> |     <link rel="author" href="humans.txt" /> | ||||||
|     <link rel="canonical" href="https://it-tools.tech" /> |     <link rel="canonical" href="https://it-tools.tech" /> | ||||||
|  |  | ||||||
|     <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> |     <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" /> | ||||||
|     <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> |     <link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" /> | ||||||
|     <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> |     <link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" /> | ||||||
|     <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#18a058" /> |     <link rel="mask-icon" href="safari-pinned-tab.svg" color="#18a058" /> | ||||||
|     <meta name="msapplication-TileColor" content="#da532c" /> |     <meta name="msapplication-TileColor" content="#da532c" /> | ||||||
|     <meta name="theme-color" content="#ffffff" /> |     <meta name="theme-color" content="#ffffff" /> | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								locales/en.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								locales/en.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | home: | ||||||
|  |   categories: | ||||||
|  |     newestTools: Newest tools | ||||||
|  |     allTheTools: All the tools | ||||||
|  |     yourFavoriteTools: Your favorite tools | ||||||
							
								
								
									
										3
									
								
								locales/fr.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								locales/fr.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | home: | ||||||
|  |   categories: | ||||||
|  |     newestTools: "Nouveaux outils" | ||||||
							
								
								
									
										4
									
								
								netlify.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								netlify.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | [[redirects]] | ||||||
|  |   from = "/*" | ||||||
|  |   to = "/index.html" | ||||||
|  |   status = 200 | ||||||
							
								
								
									
										101
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "it-tools", |   "name": "it-tools", | ||||||
|   "version": "2023.4.13-dce9ff9", |   "version": "2023.8.21-6f93cba", | ||||||
|   "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", | ||||||
| @@ -21,45 +21,52 @@ | |||||||
|   }, |   }, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "vite", |     "dev": "vite", | ||||||
|     "build": "vue-tsc --noEmit && vite build", |     "build": "vue-tsc --noEmit && NODE_OPTIONS=--max_old_space_size=4096 vite build", | ||||||
|     "preview": "vite preview --port 5050", |     "preview": "vite preview --port 5050", | ||||||
|     "test": "npm run test:unit", |     "test": "npm run test:unit", | ||||||
|     "test:unit": "vitest --environment jsdom", |     "test:unit": "vitest --environment jsdom", | ||||||
|     "test:e2e": "playwright test", |     "test:e2e": "playwright test", | ||||||
|  |     "test:e2e:dev": "BASE_URL=http://localhost:5173 NO_WEB_SERVER=true playwright test", | ||||||
|     "coverage": "vitest run --coverage", |     "coverage": "vitest run --coverage", | ||||||
|     "typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", |     "typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", | ||||||
|     "lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore", |     "lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore", | ||||||
|     "script:create-new-tool": "node scripts/create-tool.mjs", |     "script:create-new-tool": "node scripts/create-tool.mjs", | ||||||
|     "release": "standard-version" |     "release": "node ./scripts/release.mjs" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@it-tools/bip39": "^0.0.4", |     "@it-tools/bip39": "^0.0.4", | ||||||
|     "@it-tools/oggen": "^1.3.0", |     "@it-tools/oggen": "^1.3.0", | ||||||
|     "@sindresorhus/slugify": "^2.2.0", |     "@sindresorhus/slugify": "^2.2.0", | ||||||
|     "@tiptap/pm": "2.0.0-beta.220", |     "@tiptap/pm": "^2.1.6", | ||||||
|     "@tiptap/starter-kit": "2.0.0-beta.220", |     "@tiptap/starter-kit": "^2.1.6", | ||||||
|     "@tiptap/vue-3": "2.0.0-beta.220", |     "@tiptap/vue-3": "^2.0.3", | ||||||
|     "@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.3.0", | ||||||
|     "@vueuse/head": "^0.7.13", |     "@vueuse/head": "^1.0.0", | ||||||
|     "@vueuse/router": "^9.13.0", |     "@vueuse/router": "^10.0.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.1.0", | ||||||
|     "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", | ||||||
|     "date-fns": "^2.29.3", |     "date-fns": "^2.29.3", | ||||||
|  |     "emojilib": "^3.0.10", | ||||||
|     "figue": "^1.2.0", |     "figue": "^1.2.0", | ||||||
|     "fuse.js": "^6.6.2", |     "fuse.js": "^6.6.2", | ||||||
|     "highlight.js": "^11.7.0", |     "highlight.js": "^11.7.0", | ||||||
|  |     "iarna-toml-esm": "^3.0.5", | ||||||
|  |     "ibantools": "^4.3.3", | ||||||
|     "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": "^11.9.1", | ||||||
|     "mime-types": "^2.1.35", |     "mime-types": "^2.1.35", | ||||||
|  |     "monaco-editor": "^0.41.0", | ||||||
|     "naive-ui": "^2.34.3", |     "naive-ui": "^2.34.3", | ||||||
|     "netmask": "^2.0.2", |     "netmask": "^2.0.2", | ||||||
|     "node-forge": "^1.3.1", |     "node-forge": "^1.3.1", | ||||||
| @@ -68,59 +75,61 @@ | |||||||
|     "plausible-tracker": "^0.3.8", |     "plausible-tracker": "^0.3.8", | ||||||
|     "qrcode": "^1.5.1", |     "qrcode": "^1.5.1", | ||||||
|     "randombytes": "^2.1.0", |     "randombytes": "^2.1.0", | ||||||
|     "sql-formatter": "^8.2.0", |     "sql-formatter": "^13.0.0", | ||||||
|     "ts-pattern": "^4.2.2", |  | ||||||
|     "ua-parser-js": "^1.0.35", |     "ua-parser-js": "^1.0.35", | ||||||
|     "uuid": "^8.3.2", |     "unicode-emoji-json": "^0.4.0", | ||||||
|     "vue": "^3.2.47", |     "unplugin-auto-import": "^0.16.4", | ||||||
|  |     "uuid": "^9.0.0", | ||||||
|  |     "vue": "^3.3.4", | ||||||
|  |     "vue-i18n": "^9.2.2", | ||||||
|     "vue-router": "^4.1.6", |     "vue-router": "^4.1.6", | ||||||
|  |     "vue-tsc": "^1.8.1", | ||||||
|  |     "xml-formatter": "^3.3.2", | ||||||
|     "yaml": "^2.2.1" |     "yaml": "^2.2.1" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  |     "@antfu/eslint-config": "^0.41.0", | ||||||
|  |     "@iconify-json/mdi": "^1.1.50", | ||||||
|  |     "@intlify/unplugin-vue-i18n": "^0.13.0", | ||||||
|     "@playwright/test": "^1.32.3", |     "@playwright/test": "^1.32.3", | ||||||
|     "@rushstack/eslint-patch": "^1.2.0", |     "@rushstack/eslint-patch": "^1.2.0", | ||||||
|  |     "@tsconfig/node18": "^18.2.0", | ||||||
|     "@types/bcryptjs": "^2.4.2", |     "@types/bcryptjs": "^2.4.2", | ||||||
|     "@types/crypto-js": "^4.1.1", |     "@types/crypto-js": "^4.1.1", | ||||||
|     "@types/jsdom": "^16.2.15", |     "@types/jsdom": "^21.0.0", | ||||||
|     "@types/lodash": "^4.14.192", |     "@types/lodash": "^4.14.192", | ||||||
|     "@types/mime-types": "^2.1.1", |     "@types/mime-types": "^2.1.1", | ||||||
|     "@types/netmask": "^2.0.0", |     "@types/netmask": "^2.0.0", | ||||||
|     "@types/node": "^16.18.23", |     "@types/node": "^18.15.11", | ||||||
|     "@types/node-forge": "^1.3.2", |     "@types/node-forge": "^1.3.2", | ||||||
|     "@types/prettier": "^2.7.2", |  | ||||||
|     "@types/qrcode": "^1.5.0", |     "@types/qrcode": "^1.5.0", | ||||||
|     "@types/randombytes": "^2.0.0", |     "@types/randombytes": "^2.0.0", | ||||||
|     "@types/ua-parser-js": "^0.7.36", |     "@types/ua-parser-js": "^0.7.36", | ||||||
|     "@types/uuid": "^8.3.4", |     "@types/uuid": "^9.0.0", | ||||||
|     "@typescript-eslint/parser": "^5.58.0", |     "@unocss/eslint-config": "^0.55.0", | ||||||
|     "@unocss/eslint-config": "^0.50.8", |     "@vitejs/plugin-vue": "^4.3.2", | ||||||
|     "@vitejs/plugin-vue": "^2.3.4", |     "@vitejs/plugin-vue-jsx": "^3.0.2", | ||||||
|     "@vitejs/plugin-vue-jsx": "^1.3.10", |     "@vue/compiler-sfc": "^3.2.47", | ||||||
|     "@vue/eslint-config-prettier": "^7.1.0", |     "@vue/runtime-dom": "^3.3.4", | ||||||
|     "@vue/eslint-config-typescript": "^10.0.0", |  | ||||||
|     "@vue/test-utils": "^2.3.2", |     "@vue/test-utils": "^2.3.2", | ||||||
|     "@vue/tsconfig": "^0.1.3", |     "@vue/tsconfig": "^0.4.0", | ||||||
|     "c8": "^7.13.0", |     "c8": "^8.0.0", | ||||||
|     "eslint": "^8.38.0", |     "consola": "^3.0.2", | ||||||
|     "eslint-config-prettier": "^8.8.0", |     "eslint": "^8.47.0", | ||||||
|     "eslint-import-resolver-typescript": "^3.5.5", |     "jsdom": "^22.0.0", | ||||||
|     "eslint-plugin-import": "^2.27.5", |  | ||||||
|     "eslint-plugin-vue": "^8.7.1", |  | ||||||
|     "jsdom": "^19.0.0", |  | ||||||
|     "less": "^4.1.3", |     "less": "^4.1.3", | ||||||
|     "prettier": "^2.8.7", |     "prettier": "^3.0.0", | ||||||
|     "standard-version": "^9.5.0", |     "typescript": "~5.2.0", | ||||||
|     "start-server-and-test": "^1.15.4", |     "unocss": "^0.55.0", | ||||||
|     "typescript": "~4.5.5", |     "unocss-preset-scrollbar": "^0.2.1", | ||||||
|     "unocss": "^0.50.8", |     "unplugin-icons": "^0.17.0", | ||||||
|     "unplugin-auto-import": "^0.15.2", |     "unplugin-vue-components": "^0.25.0", | ||||||
|     "unplugin-vue-components": "^0.24.1", |     "vite": "^4.4.9", | ||||||
|     "vite": "^2.9.15", |     "vite-plugin-pwa": "^0.16.0", | ||||||
|     "vite-plugin-md": "^0.12.4", |     "vite-plugin-vue-markdown": "^0.23.5", | ||||||
|     "vite-plugin-pwa": "^0.11.13", |     "vite-svg-loader": "^4.0.0", | ||||||
|     "vite-svg-loader": "^3.6.0", |     "vitest": "^0.34.0", | ||||||
|     "vitest": "^0.13.1", |     "workbox-window": "^7.0.0", | ||||||
|     "vue-tsc": "^0.31.4", |     "zx": "^7.2.1" | ||||||
|     "workbox-window": "^6.5.4" |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,8 @@ | |||||||
| import { defineConfig, devices } from '@playwright/test'; | import { defineConfig, devices } from '@playwright/test'; | ||||||
|  |  | ||||||
| /** | const isCI = !!process.env.CI; | ||||||
|  * Read environment variables from file. | const baseUrl = process.env.BASE_URL || 'http://localhost:5050'; | ||||||
|  * https://github.com/motdotla/dotenv | const useWebServer = process.env.NO_WEB_SERVER !== 'true'; | ||||||
|  */ |  | ||||||
| // require('dotenv').config(); |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * See https://playwright.dev/docs/test-configuration. |  * See https://playwright.dev/docs/test-configuration. | ||||||
| @@ -15,17 +13,17 @@ export default defineConfig({ | |||||||
|   /* Run tests in files in parallel */ |   /* Run tests in files in parallel */ | ||||||
|   fullyParallel: true, |   fullyParallel: true, | ||||||
|   /* Fail the build on CI if you accidentally left test.only in the source code. */ |   /* Fail the build on CI if you accidentally left test.only in the source code. */ | ||||||
|   forbidOnly: !!process.env.CI, |   forbidOnly: isCI, | ||||||
|   /* Retry on CI only */ |   /* Retry on CI only */ | ||||||
|   retries: process.env.CI ? 2 : 0, |   retries: isCI ? 2 : 0, | ||||||
|   /* Opt out of parallel tests on CI. */ |   /* Opt out of parallel tests on CI. */ | ||||||
|   workers: process.env.CI ? 1 : undefined, |   workers: isCI ? 1 : undefined, | ||||||
|   /* Reporter to use. See https://playwright.dev/docs/test-reporters */ |   /* Reporter to use. See https://playwright.dev/docs/test-reporters */ | ||||||
|   reporter: 'html', |   reporter: 'html', | ||||||
|   /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ |   /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ | ||||||
|   use: { |   use: { | ||||||
|     /* Base URL to use in actions like `await page.goto('/')`. */ |     /* Base URL to use in actions like `await page.goto('/')`. */ | ||||||
|     baseURL: 'http://127.0.0.1:3000', |     baseURL: baseUrl, | ||||||
|  |  | ||||||
|     /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ |     /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ | ||||||
|     trace: 'on-first-retry', |     trace: 'on-first-retry', | ||||||
| @@ -51,32 +49,17 @@ export default defineConfig({ | |||||||
|       name: 'webkit', |       name: 'webkit', | ||||||
|       use: { ...devices['Desktop Safari'] }, |       use: { ...devices['Desktop Safari'] }, | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     /* Test against mobile viewports. */ |  | ||||||
|     // { |  | ||||||
|     //   name: 'Mobile Chrome', |  | ||||||
|     //   use: { ...devices['Pixel 5'] }, |  | ||||||
|     // }, |  | ||||||
|     // { |  | ||||||
|     //   name: 'Mobile Safari', |  | ||||||
|     //   use: { ...devices['iPhone 12'] }, |  | ||||||
|     // }, |  | ||||||
|  |  | ||||||
|     /* Test against branded browsers. */ |  | ||||||
|     // { |  | ||||||
|     //   name: 'Microsoft Edge', |  | ||||||
|     //   use: { ...devices['Desktop Edge'], channel: 'msedge' }, |  | ||||||
|     // }, |  | ||||||
|     // { |  | ||||||
|     //   name: 'Google Chrome', |  | ||||||
|     //   use: { ..devices['Desktop Chrome'], channel: 'chrome' }, |  | ||||||
|     // }, |  | ||||||
|   ], |   ], | ||||||
|  |  | ||||||
|   /* Run your local dev server before starting the tests */ |   /* Run your local dev server before starting the tests */ | ||||||
|  |  | ||||||
|  |   ...(useWebServer | ||||||
|  |     && { | ||||||
|       webServer: { |       webServer: { | ||||||
|     command: 'npm run dev', |         command: 'npm run preview', | ||||||
|     url: 'http://127.0.0.1:3000', |         url: 'http://127.0.0.1:5050', | ||||||
|     reuseExistingServer: !process.env.CI, |         reuseExistingServer: !isCI, | ||||||
|       }, |       }, | ||||||
|  |     } | ||||||
|  |   ), | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										7694
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7694
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										6
									
								
								renovate.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								renovate.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | { | ||||||
|  |   "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||||
|  |   "extends": [ | ||||||
|  |     "config:base" | ||||||
|  |   ] | ||||||
|  | } | ||||||
| @@ -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"> | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								scripts/getLatestChangelog.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								scripts/getLatestChangelog.mjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | import { readFile } from 'fs/promises'; | ||||||
|  |  | ||||||
|  | const changelogContent = await readFile('./CHANGELOG.md', 'utf-8'); | ||||||
|  | const [, lastChangelog] = changelogContent.split(/^## .*$/gm); | ||||||
|  |  | ||||||
|  | console.log(lastChangelog.trim()); | ||||||
							
								
								
									
										57
									
								
								scripts/release.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								scripts/release.mjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | import { $, argv } from 'zx'; | ||||||
|  | import { consola } from 'consola'; | ||||||
|  | import { rawCommitsToMarkdown } from './shared/commits.mjs'; | ||||||
|  | import { addToChangelog } from './shared/changelog.mjs'; | ||||||
|  |  | ||||||
|  | $.verbose = false; | ||||||
|  |  | ||||||
|  | const isDryRun = argv['dry-run'] ?? false; | ||||||
|  |  | ||||||
|  | const now = new Date(); | ||||||
|  | const currentShortSha = (await $`git rev-parse --short HEAD`).stdout.trim(); | ||||||
|  |  | ||||||
|  | const calver = now.toISOString().slice(0, 10).replace(/-/g, '.'); | ||||||
|  | const version = `${calver}-${currentShortSha}`; | ||||||
|  |  | ||||||
|  | const { stdout: rawCommits } = await $`git log --pretty=oneline $(git describe --tags --abbrev=0)..HEAD`; | ||||||
|  |  | ||||||
|  | const markdown = rawCommitsToMarkdown({ rawCommits }); | ||||||
|  |  | ||||||
|  | consola.info(`Changelog: \n\n${markdown}\n\n`); | ||||||
|  |  | ||||||
|  | if (isDryRun) { | ||||||
|  |   consola.info(`[dry-run] Not creating version nor tag`); | ||||||
|  |   consola.info('Aborting'); | ||||||
|  |   process.exit(0); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const shouldContinue = await consola.prompt( | ||||||
|  |   'This script will create a new version and tag, and update the changelog. Continue?', | ||||||
|  |   { | ||||||
|  |     type: 'confirm', | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | if (!shouldContinue) { | ||||||
|  |   consola.info('Aborting'); | ||||||
|  |   process.exit(0); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | consola.info('Updating changelog'); | ||||||
|  | await addToChangelog({ changelog: markdown, version }); | ||||||
|  | consola.success('Changelog updated'); | ||||||
|  |  | ||||||
|  | try { | ||||||
|  |   consola.info('Committing changelog changes'); | ||||||
|  |   await $`git add CHANGELOG.md`; | ||||||
|  |   await $`git commit -m "docs(changelog): update changelog for ${version}"`; | ||||||
|  |   consola.success('Changelog changes committed'); | ||||||
|  |  | ||||||
|  |   consola.info('Creating version and tag'); | ||||||
|  |   await $`npm version ${version} -m "chore(version): release ${version}"`; | ||||||
|  |   consola.info('Npm version released with tag'); | ||||||
|  | } catch (error) { | ||||||
|  |   consola.error(error); | ||||||
|  |   consola.info('Aborting'); | ||||||
|  |   process.exit(1); | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								scripts/shared/changelog.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								scripts/shared/changelog.mjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | import { readFile, writeFile } from 'fs/promises'; | ||||||
|  |  | ||||||
|  | export { addToChangelog }; | ||||||
|  |  | ||||||
|  | async function addToChangelog({ changelog, version, changelogPath = './CHANGELOG.md' }) { | ||||||
|  |   const changelogContent = await readFile(changelogPath, 'utf-8'); | ||||||
|  |   const versionTitle = `## Version ${version}`; | ||||||
|  |  | ||||||
|  |   if (changelogContent.includes(versionTitle)) { | ||||||
|  |     throw new Error(`Version ${version} already exists in the changelog`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const newChangeLogContent = changelogContent.replace('## ', `${versionTitle}\n\n${changelog}\n\n## `); | ||||||
|  |   await writeFile(changelogPath, newChangeLogContent, 'utf-8'); | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								scripts/shared/commits.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								scripts/shared/commits.mjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | import _ from 'lodash'; | ||||||
|  |  | ||||||
|  | export { rawCommitsToMarkdown }; | ||||||
|  |  | ||||||
|  | const commitScopesToHumanReadable = { | ||||||
|  |   build: 'Build system', | ||||||
|  |   chore: 'Chores', | ||||||
|  |   ci: 'Continuous integration', | ||||||
|  |   docs: 'Documentation', | ||||||
|  |   feat: 'Features', | ||||||
|  |   fix: 'Bug fixes', | ||||||
|  |   infra: 'Infrastucture', | ||||||
|  |   perf: 'Performance', | ||||||
|  |   refactor: 'Refactoring', | ||||||
|  |   test: 'Tests', | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const commitTypesOrder = ['feat', 'fix', 'perf', 'refactor', 'test', 'build', 'ci', 'chore', 'other']; | ||||||
|  |  | ||||||
|  | const getCommitTypeSortIndex = (type) => | ||||||
|  |   commitTypesOrder.includes(type) ? commitTypesOrder.indexOf(type) : commitTypesOrder.length; | ||||||
|  |  | ||||||
|  | function parseCommitLine(commit) { | ||||||
|  |   const [sha, ...splittedRawMessage] = commit.trim().split(' '); | ||||||
|  |   const rawMessage = splittedRawMessage.join(' '); | ||||||
|  |   const { type, scope, subject } = /^(?<type>.*?)(\((?<scope>.*)\))?: ?(?<subject>.+)$/.exec(rawMessage)?.groups ?? {}; | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     sha: sha.slice(0, 7), | ||||||
|  |     type: type ?? 'other', | ||||||
|  |     scope, | ||||||
|  |     subject: subject ?? rawMessage, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function commitSectionsToMarkdown({ type, commits }) { | ||||||
|  |   return [ | ||||||
|  |     `### ${commitScopesToHumanReadable[type] ?? _.capitalize(type)}`, | ||||||
|  |     ...commits.map(({ sha, scope, subject }) => ['-', scope ? `**${scope}**:` : '', subject, `(${sha})`].join(' ')), | ||||||
|  |   ].join('\n'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function rawCommitsToMarkdown({ rawCommits }) { | ||||||
|  |   return _.chain(rawCommits) | ||||||
|  |     .trim() | ||||||
|  |     .split('\n') | ||||||
|  |     .map(parseCommitLine) | ||||||
|  |     .groupBy('type') | ||||||
|  |     .map((commits, type) => ({ type, commits })) | ||||||
|  |     .sortBy(({ type }) => getCommitTypeSortIndex(type)) | ||||||
|  |     .map(commitSectionsToMarkdown) | ||||||
|  |     .join('\n\n') | ||||||
|  |     .value(); | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								src/App.vue
									
									
									
									
									
								
							| @@ -1,7 +1,6 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { computed } from 'vue'; | import { RouterView, useRoute } from 'vue-router'; | ||||||
| import { useRoute, RouterView } from 'vue-router'; | import { NGlobalStyle, NMessageProvider, NNotificationProvider, darkTheme } from 'naive-ui'; | ||||||
| import { darkTheme, NGlobalStyle, NMessageProvider, NNotificationProvider } 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 +15,14 @@ const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrid | |||||||
|  |  | ||||||
| <template> | <template> | ||||||
|   <n-config-provider :theme="theme" :theme-overrides="themeOverrides"> |   <n-config-provider :theme="theme" :theme-overrides="themeOverrides"> | ||||||
|     <n-global-style /> |     <NGlobalStyle /> | ||||||
|     <n-message-provider placement="bottom"> |     <NMessageProvider placement="bottom"> | ||||||
|       <n-notification-provider placement="bottom-right"> |       <NNotificationProvider placement="bottom-right"> | ||||||
|         <component :is="layout"> |         <component :is="layout"> | ||||||
|           <router-view /> |           <RouterView /> | ||||||
|         </component> |         </component> | ||||||
|       </n-notification-provider> |       </NNotificationProvider> | ||||||
|     </n-message-provider> |     </NMessageProvider> | ||||||
|   </n-config-provider> |   </n-config-provider> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,39 +1,9 @@ | |||||||
| <template> |  | ||||||
|   <div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name"> |  | ||||||
|     <n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })"> |  | ||||||
|       <n-icon :component="ChevronRight" :class="{ rotated: isCollapsed }" size="16" /> |  | ||||||
|  |  | ||||||
|       <span> |  | ||||||
|         {{ name }} |  | ||||||
|       </span> |  | ||||||
|     </n-text> |  | ||||||
|  |  | ||||||
|     <n-collapse-transition :show="!isCollapsed"> |  | ||||||
|       <div class="menu-wrapper"> |  | ||||||
|         <div class="toggle-bar" @click="toggleCategoryCollapse({ name })" /> |  | ||||||
|  |  | ||||||
|         <n-menu |  | ||||||
|           class="menu" |  | ||||||
|           :value="(route.name as string)" |  | ||||||
|           :collapsed-width="64" |  | ||||||
|           :collapsed-icon-size="22" |  | ||||||
|           :options="tools" |  | ||||||
|           :indent="8" |  | ||||||
|           :default-expand-all="true" |  | ||||||
|         /> |  | ||||||
|       </div> |  | ||||||
|     </n-collapse-transition> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { Tool, ToolCategory } from '@/tools/tools.types'; |  | ||||||
| import { ChevronRight } from '@vicons/tabler'; |  | ||||||
| import { useStorage } from '@vueuse/core'; | import { useStorage } from '@vueuse/core'; | ||||||
| import { useThemeVars } from 'naive-ui'; | import { useThemeVars } from 'naive-ui'; | ||||||
| import { toRefs, computed, h } from 'vue'; |  | ||||||
| import { RouterLink, useRoute } from 'vue-router'; | import { RouterLink, useRoute } from 'vue-router'; | ||||||
| import MenuIconItem from './MenuIconItem.vue'; | import MenuIconItem from './MenuIconItem.vue'; | ||||||
|  | import type { Tool, ToolCategory } from '@/tools/tools.types'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] }); | const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] }); | ||||||
| const { toolsByCategory } = toRefs(props); | const { toolsByCategory } = toRefs(props); | ||||||
| @@ -49,8 +19,8 @@ const collapsedCategories = useStorage<Record<string, boolean>>( | |||||||
|   { |   { | ||||||
|     deep: true, |     deep: true, | ||||||
|     serializer: { |     serializer: { | ||||||
|       read: (v) => (v ? JSON.parse(v) : null), |       read: v => (v ? JSON.parse(v) : null), | ||||||
|       write: (v) => JSON.stringify(v), |       write: v => JSON.stringify(v), | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| ); | ); | ||||||
| @@ -61,9 +31,9 @@ function toggleCategoryCollapse({ name }: { name: string }) { | |||||||
|  |  | ||||||
| const menuOptions = computed(() => | const menuOptions = computed(() => | ||||||
|   toolsByCategory.value.map(({ name, components }) => ({ |   toolsByCategory.value.map(({ name, components }) => ({ | ||||||
|     name: name, |     name, | ||||||
|     isCollapsed: collapsedCategories.value[name], |     isCollapsed: collapsedCategories.value[name], | ||||||
|     tools: components.map((tool) => ({ |     tools: components.map(tool => ({ | ||||||
|       label: makeLabel(tool), |       label: makeLabel(tool), | ||||||
|       icon: makeIcon(tool), |       icon: makeIcon(tool), | ||||||
|       key: tool.name, |       key: tool.name, | ||||||
| @@ -74,27 +44,37 @@ const menuOptions = computed(() => | |||||||
| const themeVars = useThemeVars(); | const themeVars = useThemeVars(); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name"> | ||||||
|  |     <div ml-6px mt-12px flex cursor-pointer items-center op-60 @click="toggleCategoryCollapse({ name })"> | ||||||
|  |       <span :class="{ 'rotate-0': isCollapsed, 'rotate-90': !isCollapsed }" text-16px lh-1 op-50 transition-transform> | ||||||
|  |         <icon-mdi-chevron-right /> | ||||||
|  |       </span> | ||||||
|  |  | ||||||
|  |       <span ml-8px text-13px> | ||||||
|  |         {{ name }} | ||||||
|  |       </span> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <n-collapse-transition :show="!isCollapsed"> | ||||||
|  |       <div class="menu-wrapper"> | ||||||
|  |         <div class="toggle-bar" @click="toggleCategoryCollapse({ name })" /> | ||||||
|  |  | ||||||
|  |         <n-menu | ||||||
|  |           class="menu" | ||||||
|  |           :value="route.name as string" | ||||||
|  |           :collapsed-width="64" | ||||||
|  |           :collapsed-icon-size="22" | ||||||
|  |           :options="tools" | ||||||
|  |           :indent="8" | ||||||
|  |           :default-expand-all="true" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </n-collapse-transition> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
| <style scoped lang="less"> | <style scoped lang="less"> | ||||||
| .category-name { |  | ||||||
|   font-size: 0.93em; |  | ||||||
|   padding: 12px 0 0px 0; |  | ||||||
|   cursor: pointer; |  | ||||||
|  |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: row; |  | ||||||
|   align-items: center; |  | ||||||
|   .n-icon { |  | ||||||
|     transition: transform ease 0.5s; |  | ||||||
|     transform: rotate(90deg); |  | ||||||
|     margin: 0 10px 0 7px; |  | ||||||
|     opacity: 0.5; |  | ||||||
|  |  | ||||||
|     &.rotated { |  | ||||||
|       transform: rotate(0deg); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .menu-wrapper { | .menu-wrapper { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: row; |   flex-direction: row; | ||||||
|   | |||||||
| @@ -1,8 +1,13 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import type { Component } from 'vue'; | ||||||
|  |  | ||||||
|  | const props = defineProps<{ icon: Component; title: string }>(); | ||||||
|  | const { icon, title } = toRefs(props); | ||||||
|  | </script> | ||||||
|  |  | ||||||
| <template> | <template> | ||||||
|   <n-card class="colored-card"> |   <c-card class="colored-card"> | ||||||
|     <n-space justify="space-between" align="center"> |  | ||||||
|     <n-icon class="icon" size="40" :component="icon" /> |     <n-icon class="icon" size="40" :component="icon" /> | ||||||
|     </n-space> |  | ||||||
|     <n-h3 class="title"> |     <n-h3 class="title"> | ||||||
|       <n-ellipsis>{{ title }}</n-ellipsis> |       <n-ellipsis>{{ title }}</n-ellipsis> | ||||||
|     </n-h3> |     </n-h3> | ||||||
| @@ -12,16 +17,9 @@ | |||||||
|         <slot /> |         <slot /> | ||||||
|       </n-ellipsis> |       </n-ellipsis> | ||||||
|     </div> |     </div> | ||||||
|   </n-card> |   </c-card> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { toRefs, type Component } from 'vue'; |  | ||||||
|  |  | ||||||
| const props = defineProps<{ icon: Component; title: string }>(); |  | ||||||
| const { icon, title } = toRefs(props); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .colored-card { | .colored-card { | ||||||
|   background: rgb(37, 99, 108); |   background: rgb(37, 99, 108); | ||||||
|   | |||||||
| @@ -1,25 +1,13 @@ | |||||||
| <template> |  | ||||||
|   <n-tooltip trigger="hover"> |  | ||||||
|     <template #trigger> |  | ||||||
|       <n-button circle quaternary :type="buttonType" :style="{ opacity: isFavorite ? 1 : 0.2 }" @click="toggleFavorite"> |  | ||||||
|         <template #icon> |  | ||||||
|           <n-icon :component="FavoriteFilled" /> |  | ||||||
|         </template> |  | ||||||
|       </n-button> |  | ||||||
|     </template> |  | ||||||
|     {{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }} |  | ||||||
|   </n-tooltip> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { FavoriteFilled } from '@vicons/material'; | import { FavoriteFilled } from '@vicons/material'; | ||||||
|  |  | ||||||
| import { useToolStore } from '@/tools/tools.store'; | import { useToolStore } from '@/tools/tools.store'; | ||||||
| import type { Tool } from '@/tools/tools.types'; | import type { Tool } from '@/tools/tools.types'; | ||||||
| import { computed, toRefs } from 'vue'; |  | ||||||
|  | const props = defineProps<{ tool: Tool }>(); | ||||||
|  |  | ||||||
| const toolStore = useToolStore(); | const toolStore = useToolStore(); | ||||||
|  |  | ||||||
| const props = defineProps<{ tool: Tool }>(); |  | ||||||
| const { tool } = toRefs(props); | const { tool } = toRefs(props); | ||||||
|  |  | ||||||
| const isFavorite = computed(() => toolStore.isToolFavorite({ tool })); | const isFavorite = computed(() => toolStore.isToolFavorite({ tool })); | ||||||
| @@ -36,3 +24,20 @@ function toggleFavorite(event: MouseEvent) { | |||||||
|   toolStore.addToolToFavorites({ tool }); |   toolStore.addToolToFavorites({ tool }); | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <n-tooltip trigger="hover"> | ||||||
|  |     <template #trigger> | ||||||
|  |       <c-button | ||||||
|  |         variant="text" | ||||||
|  |         circle | ||||||
|  |         :type="buttonType" | ||||||
|  |         :style="{ opacity: isFavorite ? 1 : 0.2 }" | ||||||
|  |         @click="toggleFavorite" | ||||||
|  |       > | ||||||
|  |         <n-icon :component="FavoriteFilled" /> | ||||||
|  |       </c-button> | ||||||
|  |     </template> | ||||||
|  |     {{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }} | ||||||
|  |   </n-tooltip> | ||||||
|  | </template> | ||||||
|   | |||||||
| @@ -1,36 +1,17 @@ | |||||||
| <template> |  | ||||||
|   <n-form-item :label="inputLabel" v-bind="validationAttrs"> |  | ||||||
|     <n-input |  | ||||||
|       ref="inputElement" |  | ||||||
|       v-model:value="input" |  | ||||||
|       :placeholder="inputPlaceholder" |  | ||||||
|       type="textarea" |  | ||||||
|       rows="20" |  | ||||||
|       autocomplete="off" |  | ||||||
|       autocorrect="off" |  | ||||||
|       autocapitalize="off" |  | ||||||
|       spellcheck="false" |  | ||||||
|       :input-props="{ 'data-test-id': 'input' }" |  | ||||||
|     /> |  | ||||||
|   </n-form-item> |  | ||||||
|   <n-form-item :label="outputLabel"> |  | ||||||
|     <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement" /> |  | ||||||
|   </n-form-item> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useValidation, type UseValidationRule } from '@/composable/validation'; |  | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
|  | import type { UseValidationRule } from '@/composable/validation'; | ||||||
|  | import CInputText from '@/ui/c-input-text/c-input-text.vue'; | ||||||
|  |  | ||||||
| const props = withDefaults( | const props = withDefaults( | ||||||
|   defineProps<{ |   defineProps<{ | ||||||
|     transformer?: (v: string) => string; |     transformer?: (v: string) => string | ||||||
|     inputValidationRules?: UseValidationRule<string>[]; |     inputValidationRules?: UseValidationRule<string>[] | ||||||
|     inputLabel?: string; |     inputLabel?: string | ||||||
|     inputPlaceholder?: string; |     inputPlaceholder?: string | ||||||
|     inputDefault?: string; |     inputDefault?: string | ||||||
|     outputLabel?: string; |     outputLabel?: string | ||||||
|     outputLanguage?: string; |     outputLanguage?: string | ||||||
|   }>(), |   }>(), | ||||||
|   { |   { | ||||||
|     transformer: _.identity, |     transformer: _.identity, | ||||||
| @@ -43,15 +24,34 @@ 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" | ||||||
|  |     monospace | ||||||
|  |   /> | ||||||
|  |  | ||||||
|  |   <div> | ||||||
|  |     <div mb-5px> | ||||||
|  |       {{ outputLabel }} | ||||||
|  |     </div> | ||||||
|  |     <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|   | |||||||
| @@ -1,43 +1,26 @@ | |||||||
| <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 { useVModel } from '@vueuse/core'; | ||||||
| import { ContentCopyFilled } from '@vicons/material'; | import { useCopy } from '@/composable/copy'; | ||||||
| import { ref } from 'vue'; |  | ||||||
|  |  | ||||||
| const props = defineProps<{ value: string }>(); | const props = defineProps<{ value: string }>(); | ||||||
| const emit = defineEmits(['update:value']); | const emit = defineEmits(['update:value']); | ||||||
|  |  | ||||||
| const value = useVModel(props, 'value', emit); | const value = useVModel(props, 'value', emit); | ||||||
| const tooltipText = ref('Copy to clipboard'); | const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||||
|  | const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : 'Copy to clipboard'); | ||||||
| const { copy } = useClipboard({ source: value }); |  | ||||||
|  |  | ||||||
| function onCopyClicked() { |  | ||||||
|   copy(); |  | ||||||
|   tooltipText.value = 'Copied !'; |  | ||||||
|  |  | ||||||
|   setTimeout(() => { |  | ||||||
|     tooltipText.value = 'Copy to clipboard'; |  | ||||||
|   }, 2000); |  | ||||||
| } |  | ||||||
| </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="copy()"> | ||||||
|  |             <icon-mdi-content-copy /> | ||||||
|  |           </c-button> | ||||||
|  |         </template> | ||||||
|  |         {{ tooltipText }} | ||||||
|  |       </n-tooltip> | ||||||
|  |     </template> | ||||||
|  |   </c-input-text> | ||||||
|  | </template> | ||||||
|   | |||||||
| @@ -1,14 +1,6 @@ | |||||||
| <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 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 +8,13 @@ const { tool } = toRefs(props); | |||||||
| const theme = useThemeVars(); | const theme = useThemeVars(); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <div class="menu-icon-item"> | ||||||
|  |     <n-icon :component="tool.icon" /> | ||||||
|  |     <div v-if="tool.isNew" class="badge" /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .menu-icon-item { | .menu-icon-item { | ||||||
|   position: relative; |   position: relative; | ||||||
|   | |||||||
| @@ -1,3 +1,11 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | 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 +27,6 @@ | |||||||
|   </n-layout> |   </n-layout> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useStyleStore } from '@/stores/style.store'; |  | ||||||
| import { toRefs, computed } from 'vue'; |  | ||||||
|  |  | ||||||
| const styleStore = useStyleStore(); |  | ||||||
| const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore); |  | ||||||
| const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static')); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .overlay { | .overlay { | ||||||
|   position: absolute; |   position: absolute; | ||||||
|   | |||||||
| @@ -1,71 +1,64 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler'; | ||||||
|  | import { useStyleStore } from '@/stores/style.store'; | ||||||
|  |  | ||||||
|  | const styleStore = useStyleStore(); | ||||||
|  | const { isDarkTheme } = toRefs(styleStore); | ||||||
|  | </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="() => styleStore.toggleDark()"> | ||||||
|         <n-icon v-if="isDarkTheme" size="25" :component="Sun" /> |         <n-icon v-if="isDarkTheme" size="25" :component="Sun" /> | ||||||
|         <n-icon v-else size="25" :component="Moon" /> |         <n-icon v-else size="25" :component="Moon" /> | ||||||
|       </n-button> |       </c-button> | ||||||
|     </template> |     </template> | ||||||
|     <span v-if="isDarkTheme">Light mode</span> |     <span v-if="isDarkTheme">Light mode</span> | ||||||
|     <span v-else>Dark mode</span> |     <span v-else>Dark mode</span> | ||||||
|   </n-tooltip> |   </n-tooltip> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useStyleStore } from '@/stores/style.store'; |  | ||||||
| import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler'; |  | ||||||
| import { toRefs } from 'vue'; |  | ||||||
|  |  | ||||||
| const styleStore = useStyleStore(); |  | ||||||
| const { isDarkTheme } = toRefs(styleStore); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .n-button { | .n-button { | ||||||
|   &:not(:last-child) { |   &:not(:last-child) { | ||||||
|   | |||||||
| @@ -1,110 +0,0 @@ | |||||||
| <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 { useMagicKeys, whenever } from '@vueuse/core'; |  | ||||||
| import type { NInput } from 'naive-ui'; |  | ||||||
| import { computed, h, ref } from 'vue'; |  | ||||||
| import { useRouter } from 'vue-router'; |  | ||||||
| import SearchBarItem from './SearchBarItem.vue'; |  | ||||||
|  |  | ||||||
| const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool }); |  | ||||||
|  |  | ||||||
| const router = useRouter(); |  | ||||||
| const { tracker } = useTracker(); |  | ||||||
|  |  | ||||||
| const queryString = ref(''); |  | ||||||
| const inputEl = ref<HTMLElement>(); |  | ||||||
| const displayDropDown = ref(true); |  | ||||||
| const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac')); |  | ||||||
|  |  | ||||||
| const options = computed(() => { |  | ||||||
|   if (queryString.value === '') { |  | ||||||
|     return tools.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({ |  | ||||||
|   passive: false, |  | ||||||
|   onEventFired(e) { |  | ||||||
|     if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') { |  | ||||||
|       e.preventDefault(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (e.metaKey && e.key === 'k' && e.type === 'keydown') { |  | ||||||
|       e.preventDefault(); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| whenever(keys.ctrl_k, claimFocus); |  | ||||||
| whenever(keys.meta_k, claimFocus); |  | ||||||
| whenever(keys.escape, releaseFocus); |  | ||||||
|  |  | ||||||
| function renderOption({ tool }: { tool: Tool }) { |  | ||||||
|   return h(SearchBarItem, { tool }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function onSelect(path: string) { |  | ||||||
|   router.push(path); |  | ||||||
|   queryString.value = ''; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function claimFocus() { |  | ||||||
|   displayDropDown.value = true; |  | ||||||
|  |  | ||||||
|   inputEl.value?.focus(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function releaseFocus() { |  | ||||||
|   displayDropDown.value = false; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function onFocus() { |  | ||||||
|   tracker.trackEvent({ eventName: 'Search-bar focused' }); |  | ||||||
|   displayDropDown.value = true; |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <template> |  | ||||||
|   <div class="search-bar"> |  | ||||||
|     <n-auto-complete |  | ||||||
|       v-model:value="queryString" |  | ||||||
|       :options="options" |  | ||||||
|       :on-select="(value) => onSelect(String(value))" |  | ||||||
|       :render-label="renderOption" |  | ||||||
|       :default-value="'aa'" |  | ||||||
|       :get-show="() => displayDropDown" |  | ||||||
|       :on-focus="onFocus" |  | ||||||
|       @update:value="() => (displayDropDown = true)" |  | ||||||
|     > |  | ||||||
|       <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }"> |  | ||||||
|         <n-input |  | ||||||
|           ref="inputEl" |  | ||||||
|           round |  | ||||||
|           clearable |  | ||||||
|           :placeholder="`Search a tool (use ${isMac ? 'Cmd' : 'Ctrl'} + K to focus)`" |  | ||||||
|           :value="slotValue" |  | ||||||
|           :input-props="{ autocomplete: 'disabled' }" |  | ||||||
|           @input="handleInput" |  | ||||||
|           @focus="handleFocus" |  | ||||||
|           @blur="handleBlur" |  | ||||||
|         > |  | ||||||
|           <template #prefix> |  | ||||||
|             <n-icon :component="SearchRound" /> |  | ||||||
|           </template> |  | ||||||
|         </n-input> |  | ||||||
|       </template> |  | ||||||
|     </n-auto-complete> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| @@ -1,45 +0,0 @@ | |||||||
| <script lang="ts" setup> |  | ||||||
| import type { Tool } from '@/tools/tools.types'; |  | ||||||
| import { toRefs } from 'vue'; |  | ||||||
|  |  | ||||||
| const props = defineProps<{ tool: Tool }>(); |  | ||||||
| const { tool } = toRefs(props); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <template> |  | ||||||
|   <div class="search-bar-item"> |  | ||||||
|     <n-icon class="icon" :component="tool.icon" /> |  | ||||||
|  |  | ||||||
|     <div> |  | ||||||
|       <div class="name">{{ tool.name }}</div> |  | ||||||
|       <div class="description">{{ tool.description }}</div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <style lang="less" scoped> |  | ||||||
| .search-bar-item { |  | ||||||
|   padding: 10px; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: row; |  | ||||||
|   align-items: center; |  | ||||||
|  |  | ||||||
|   .icon { |  | ||||||
|     font-size: 30px; |  | ||||||
|     margin-right: 10px; |  | ||||||
|     opacity: 0.7; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .name { |  | ||||||
|     font-weight: bold; |  | ||||||
|     font-size: 15px; |  | ||||||
|     line-height: 1; |  | ||||||
|     margin-bottom: 5px; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .description { |  | ||||||
|     opacity: 0.7; |  | ||||||
|     line-height: 1; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
							
								
								
									
										27
									
								
								src/components/SpanCopyable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/components/SpanCopyable.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  |  | ||||||
|  | const props = withDefaults(defineProps<{ value?: string }>(), { value: '' }); | ||||||
|  | const { value } = toRefs(props); | ||||||
|  |  | ||||||
|  | const initialText = 'Copy to clipboard'; | ||||||
|  |  | ||||||
|  | const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||||
|  | const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : initialText); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <n-tooltip trigger="hover"> | ||||||
|  |     <template #trigger> | ||||||
|  |       <span class="value" @click="copy()">{{ value }}</span> | ||||||
|  |     </template> | ||||||
|  |     {{ tooltipText }} | ||||||
|  |   </n-tooltip> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <style scoped lang="less"> | ||||||
|  | .value { | ||||||
|  |   cursor: pointer; | ||||||
|  |   font-family: monospace; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -1,6 +1,46 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { Copy } from '@vicons/tabler'; | ||||||
|  | import { useElementSize } from '@vueuse/core'; | ||||||
|  | import hljs from 'highlight.js/lib/core'; | ||||||
|  | import jsonHljs from 'highlight.js/lib/languages/json'; | ||||||
|  | import sqlHljs from 'highlight.js/lib/languages/sql'; | ||||||
|  | import xmlHljs from 'highlight.js/lib/languages/xml'; | ||||||
|  | import yamlHljs from 'highlight.js/lib/languages/yaml'; | ||||||
|  | import iniHljs from 'highlight.js/lib/languages/ini'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  |  | ||||||
|  | const props = withDefaults( | ||||||
|  |   defineProps<{ | ||||||
|  |     value: string | ||||||
|  |     followHeightOf?: HTMLElement | null | ||||||
|  |     language?: string | ||||||
|  |     copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none' | ||||||
|  |     copyMessage?: string | ||||||
|  |   }>(), | ||||||
|  |   { | ||||||
|  |     followHeightOf: null, | ||||||
|  |     language: 'txt', | ||||||
|  |     copyPlacement: 'top-right', | ||||||
|  |     copyMessage: 'Copy to clipboard', | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | hljs.registerLanguage('sql', sqlHljs); | ||||||
|  | hljs.registerLanguage('json', jsonHljs); | ||||||
|  | hljs.registerLanguage('html', xmlHljs); | ||||||
|  | hljs.registerLanguage('xml', xmlHljs); | ||||||
|  | hljs.registerLanguage('yaml', yamlHljs); | ||||||
|  | hljs.registerLanguage('toml', iniHljs); | ||||||
|  |  | ||||||
|  | const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); | ||||||
|  | const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) }; | ||||||
|  |  | ||||||
|  | const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||||
|  | const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.value); | ||||||
|  | </script> | ||||||
|  |  | ||||||
| <template> | <template> | ||||||
|   <div style="overflow-x: hidden; width: 100%"> |   <div style="overflow-x: hidden; width: 100%"> | ||||||
|     <n-card class="result-card"> |     <c-card class="result-card"> | ||||||
|       <n-scrollbar |       <n-scrollbar | ||||||
|         x-scrollable |         x-scrollable | ||||||
|         trigger="none" |         trigger="none" | ||||||
| @@ -13,66 +53,22 @@ | |||||||
|       <n-tooltip v-if="value" trigger="hover"> |       <n-tooltip v-if="value" trigger="hover"> | ||||||
|         <template #trigger> |         <template #trigger> | ||||||
|           <div class="copy-button" :class="[copyPlacement]"> |           <div class="copy-button" :class="[copyPlacement]"> | ||||||
|             <n-button circle secondary size="large" @click="onCopyClicked"> |             <c-button circle important:h-10 important:w-10 @click="copy()"> | ||||||
|               <n-icon size="22" :component="Copy" /> |               <n-icon size="22" :component="Copy" /> | ||||||
|             </n-button> |             </c-button> | ||||||
|           </div> |           </div> | ||||||
|         </template> |         </template> | ||||||
|         <span>{{ tooltipText }}</span> |         <span>{{ tooltipText }}</span> | ||||||
|       </n-tooltip> |       </n-tooltip> | ||||||
|     </n-card> |     </c-card> | ||||||
|     <n-space v-if="copyPlacement === 'outside'" justify="center" mt-4> |     <div v-if="copyPlacement === 'outside'" mt-4 flex justify-center> | ||||||
|       <n-button secondary @click="onCopyClicked"> {{ tooltipText }} </n-button> |       <c-button @click="copy()"> | ||||||
|     </n-space> |         {{ tooltipText }} | ||||||
|  |       </c-button> | ||||||
|  |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { Copy } from '@vicons/tabler'; |  | ||||||
| import { useClipboard, useElementSize } from '@vueuse/core'; |  | ||||||
| import hljs from 'highlight.js/lib/core'; |  | ||||||
| import jsonHljs from 'highlight.js/lib/languages/json'; |  | ||||||
| import sqlHljs from 'highlight.js/lib/languages/sql'; |  | ||||||
| import xmlHljs from 'highlight.js/lib/languages/xml'; |  | ||||||
| import yamlHljs from 'highlight.js/lib/languages/yaml'; |  | ||||||
| import { ref, toRefs } from 'vue'; |  | ||||||
|  |  | ||||||
| hljs.registerLanguage('sql', sqlHljs); |  | ||||||
| hljs.registerLanguage('json', jsonHljs); |  | ||||||
| hljs.registerLanguage('html', xmlHljs); |  | ||||||
| hljs.registerLanguage('yaml', yamlHljs); |  | ||||||
|  |  | ||||||
| const props = withDefaults( |  | ||||||
|   defineProps<{ |  | ||||||
|     value: string; |  | ||||||
|     followHeightOf?: HTMLElement | null; |  | ||||||
|     language?: string; |  | ||||||
|     copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none'; |  | ||||||
|     copyMessage?: string; |  | ||||||
|   }>(), |  | ||||||
|   { |  | ||||||
|     followHeightOf: null, |  | ||||||
|     language: 'txt', |  | ||||||
|     copyPlacement: 'top-right', |  | ||||||
|     copyMessage: 'Copy to clipboard', |  | ||||||
|   }, |  | ||||||
| ); |  | ||||||
| const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); |  | ||||||
| const { height } = followHeightOf ? useElementSize(followHeightOf) : { height: ref(null) }; |  | ||||||
|  |  | ||||||
| const { copy } = useClipboard({ source: value }); |  | ||||||
| const tooltipText = ref(copyMessage.value); |  | ||||||
|  |  | ||||||
| function onCopyClicked() { |  | ||||||
|   copy(); |  | ||||||
|   tooltipText.value = 'Copied !'; |  | ||||||
|  |  | ||||||
|   setTimeout(() => { |  | ||||||
|     tooltipText.value = copyMessage.value; |  | ||||||
|   }, 2000); |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| ::v-deep(.n-scrollbar) { | ::v-deep(.n-scrollbar) { | ||||||
|   padding-bottom: 10px; |   padding-bottom: 10px; | ||||||
|   | |||||||
| @@ -1,9 +1,19 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useThemeVars } from 'naive-ui'; | ||||||
|  | import FavoriteButton from './FavoriteButton.vue'; | ||||||
|  | import type { Tool } from '@/tools/tools.types'; | ||||||
|  |  | ||||||
|  | const props = defineProps<{ tool: Tool & { category: string } }>(); | ||||||
|  | const { tool } = toRefs(props); | ||||||
|  | const theme = useThemeVars(); | ||||||
|  | </script> | ||||||
|  |  | ||||||
| <template> | <template> | ||||||
|   <router-link :to="tool.path"> |   <router-link :to="tool.path"> | ||||||
|     <n-card class="tool-card"> |     <c-card class="tool-card" shadow> | ||||||
|       <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,46 +26,36 @@ | |||||||
|             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" truncate> | ||||||
|         <n-ellipsis>{{ tool.name }}</n-ellipsis> |         {{ tool.name }} | ||||||
|       </n-h3> |       </n-h3> | ||||||
|  |  | ||||||
|       <div class="description"> |       <div class="description"> | ||||||
|         <n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px"> |         <div line-clamp-2 style="min-height: 44.78px"> | ||||||
|           {{ tool.description }} |           {{ tool.description }} | ||||||
|           <br />  |  | ||||||
|         </n-ellipsis> |  | ||||||
|         </div> |         </div> | ||||||
|     </n-card> |       </div> | ||||||
|  |     </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 { | ||||||
|   &:hover { |   border-width: 2px !important; | ||||||
|     border-color: var(--n-color-target); |   color: transparent; | ||||||
|   } |   position: relative; | ||||||
|  |   border-radius: 15px; | ||||||
|  |   border: none; | ||||||
|  |  | ||||||
|   .icon { |   .icon { | ||||||
|     opacity: 0.6; |     opacity: 0.4; | ||||||
|     color: v-bind('theme.textColorBase'); |     color: v-bind('theme.textColorBase'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -68,5 +68,46 @@ a { | |||||||
|     color: v-bind('theme.textColorBase'); |     color: v-bind('theme.textColorBase'); | ||||||
|     margin: 5px 0; |     margin: 5px 0; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   &::after { | ||||||
|  |     --mask-radius: 20em; | ||||||
|  |  | ||||||
|  |     border-radius: 15px; | ||||||
|  |     content: ''; | ||||||
|  |     position: absolute; | ||||||
|  |     inset: 0; | ||||||
|  |     pointer-events: none; | ||||||
|  |     user-select: none; | ||||||
|  |     display: block; | ||||||
|  |     height: calc(100% - 4px) ; | ||||||
|  |     width:  calc(100% - 4px) ; | ||||||
|  |     background: #18a05818; | ||||||
|  |     top: 0; | ||||||
|  |     left: 0; | ||||||
|  |     opacity: 1; | ||||||
|  |     border: 2px solid transparent; | ||||||
|  |     transition: all 0.2s ease-in-out; | ||||||
|  |  | ||||||
|  |     -webkit-mask: radial-gradient( | ||||||
|  |       var(--mask-radius) var(--mask-radius) at 45px 45px, | ||||||
|  |       #000 1%, | ||||||
|  |       transparent 50% | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     mask: radial-gradient( | ||||||
|  |       var(--mask-radius) var(--mask-radius) at 45px 45px, | ||||||
|  |       #000 1%, | ||||||
|  |       transparent 50% | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     will-change: mask; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &:hover { | ||||||
|  |     &::after { | ||||||
|  |       --mask-radius: 50em; | ||||||
|  |       border: 2px solid #18a058; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -11,7 +11,8 @@ function computedRefreshable<T>(getter: () => T, { throttle }: { throttle?: numb | |||||||
|  |  | ||||||
|   if (throttle) { |   if (throttle) { | ||||||
|     watchThrottled(getter, update, { throttle }); |     watchThrottled(getter, update, { throttle }); | ||||||
|   } else { |   } | ||||||
|  |   else { | ||||||
|     watch(getter, update); |     watch(getter, update); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,15 +1,30 @@ | |||||||
|  | // eslint-disable-next-line no-restricted-imports | ||||||
| import { useClipboard } from '@vueuse/core'; | import { useClipboard } from '@vueuse/core'; | ||||||
| import { useMessage } from 'naive-ui'; | import { useMessage } from 'naive-ui'; | ||||||
| import type { Ref } from 'vue'; | import type { MaybeRefOrGetter } from 'vue'; | ||||||
|  |  | ||||||
|  | export function useCopy({ source, text = 'Copied to the clipboard', createToast = true }: { source?: MaybeRefOrGetter<string>; text?: string; createToast?: boolean } = {}) { | ||||||
|  |   const { copy, copied, ...rest } = useClipboard({ | ||||||
|  |     source, | ||||||
|  |     legacy: true, | ||||||
|  |   }); | ||||||
|  |  | ||||||
| export function useCopy({ source, text = 'Copied to the clipboard' }: { source: Ref; text?: string }) { |  | ||||||
|   const { copy } = useClipboard({ source }); |  | ||||||
|   const message = useMessage(); |   const message = useMessage(); | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     async copy() { |     ...rest, | ||||||
|  |     isJustCopied: copied, | ||||||
|  |     async copy(content?: string, { notificationMessage }: { notificationMessage?: string } = {}) { | ||||||
|  |       if (source) { | ||||||
|         await copy(); |         await copy(); | ||||||
|       message.success(text); |       } | ||||||
|  |       else { | ||||||
|  |         await copy(content); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (createToast) { | ||||||
|  |         message.success(notificationMessage ?? text); | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,8 +5,8 @@ function getFileExtensionFromBase64({ | |||||||
|   base64String, |   base64String, | ||||||
|   defaultExtension = 'txt', |   defaultExtension = 'txt', | ||||||
| }: { | }: { | ||||||
|   base64String: string; |   base64String: string | ||||||
|   defaultExtension?: string; |   defaultExtension?: string | ||||||
| }) { | }) { | ||||||
|   const hasMimeType = base64String.match(/data:(.*?);base64/i); |   const hasMimeType = base64String.match(/data:(.*?);base64/i); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { get, type MaybeRef } from '@vueuse/core'; | import { type MaybeRef, get } from '@vueuse/core'; | ||||||
| import Fuse from 'fuse.js'; | import Fuse from 'fuse.js'; | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
|  |  | ||||||
| @@ -9,14 +9,21 @@ 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> & { filterEmpty?: boolean } | ||||||
| }) { | }) { | ||||||
|   const fuse = new Fuse(data, options); |   const fuse = new Fuse(data, options); | ||||||
|  |   const filterEmpty = options.filterEmpty ?? true; | ||||||
|  |  | ||||||
|   const searchResult = computed(() => { |   const searchResult = computed<Data[]>(() => { | ||||||
|     return fuse.search(get(search)).map(({ item }) => item); |     const query = get(search); | ||||||
|  |  | ||||||
|  |     if (!filterEmpty && query === '') { | ||||||
|  |       return data; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return fuse.search(query).map(({ item }) => item); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   return { searchResult }; |   return { searchResult }; | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: | |||||||
|  |  | ||||||
|   return computed<T>({ |   return computed<T>({ | ||||||
|     get() { |     get() { | ||||||
|       return transformer.fromQuery(proxy.value) as T; |       return transformer.fromQuery(proxy.value) as unknown as T; | ||||||
|     }, |     }, | ||||||
|     set(value) { |     set(value) { | ||||||
|       proxy.value = transformer.toQuery(value as never); |       proxy.value = transformer.toQuery(value as never); | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| /* eslint-disable @typescript-eslint/no-empty-function */ |  | ||||||
| import { describe, expect, it } from 'vitest'; | import { describe, expect, it } from 'vitest'; | ||||||
| import { isFalsyOrHasThrown } from './validation'; | import { isFalsyOrHasThrown } from './validation'; | ||||||
|  |  | ||||||
| @@ -11,7 +10,7 @@ describe('useValidation', () => { | |||||||
|       expect(isFalsyOrHasThrown(() => {})).toBe(true); |       expect(isFalsyOrHasThrown(() => {})).toBe(true); | ||||||
|       expect( |       expect( | ||||||
|         isFalsyOrHasThrown(() => { |         isFalsyOrHasThrown(() => { | ||||||
|           throw new Error(); |           throw new Error('message'); | ||||||
|         }), |         }), | ||||||
|       ).toBe(true); |       ).toBe(true); | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -1,44 +1,48 @@ | |||||||
|  | import { type MaybeRef, get } from '@vueuse/core'; | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| import { reactive, watch, type Ref } from 'vue'; | import { type Ref, reactive, watch } from 'vue'; | ||||||
|  |  | ||||||
| type ValidatorReturnType = unknown; | type ValidatorReturnType = unknown; | ||||||
|  |  | ||||||
| export interface UseValidationRule<T> { | export interface UseValidationRule<T> { | ||||||
|   validator: (value: T) => ValidatorReturnType; |   validator: (value: T) => ValidatorReturnType | ||||||
|   message: string; |   message: string | ||||||
| } | } | ||||||
|  |  | ||||||
| export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { | export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { | ||||||
|   try { |   try { | ||||||
|     const returnValue = cb(); |     const returnValue = cb(); | ||||||
|  |  | ||||||
|     if (_.isNil(returnValue)) return true; |     if (_.isNil(returnValue)) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return returnValue === false; |     return returnValue === false; | ||||||
|   } catch (_) { |   } | ||||||
|  |   catch (_) { | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| export type ValidationAttrs = { | export interface ValidationAttrs { | ||||||
|   feedback: string; |   feedback: string | ||||||
|   validationStatus: string | undefined; |   validationStatus: string | undefined | ||||||
| }; | } | ||||||
|  |  | ||||||
| export function useValidation<T>({ | export function useValidation<T>({ | ||||||
|   source, |   source, | ||||||
|   rules, |   rules, | ||||||
|   watch: watchRefs = [], |   watch: watchRefs = [], | ||||||
| }: { | }: { | ||||||
|   source: Ref<T>; |   source: Ref<T> | ||||||
|   rules: UseValidationRule<T>[]; |   rules: MaybeRef<UseValidationRule<T>[]> | ||||||
|   watch?: Ref<unknown>[]; |   watch?: Ref<unknown>[] | ||||||
| }) { | }) { | ||||||
|   const state = reactive<{ |   const state = reactive<{ | ||||||
|     message: string; |     message: string | ||||||
|     status: undefined | 'error'; |     status: undefined | 'error' | ||||||
|     isValid: boolean; |     isValid: boolean | ||||||
|     attrs: ValidationAttrs; |     attrs: ValidationAttrs | ||||||
|   }>({ |   }>({ | ||||||
|     message: '', |     message: '', | ||||||
|     status: undefined, |     status: undefined, | ||||||
| @@ -55,7 +59,7 @@ export function useValidation<T>({ | |||||||
|       state.message = ''; |       state.message = ''; | ||||||
|       state.status = undefined; |       state.status = undefined; | ||||||
|  |  | ||||||
|       for (const rule of rules) { |       for (const rule of get(rules)) { | ||||||
|         if (isFalsyOrHasThrown(() => rule.validator(source.value))) { |         if (isFalsyOrHasThrown(() => rule.validator(source.value))) { | ||||||
|           state.message = rule.message; |           state.message = rule.message; | ||||||
|           state.status = 'error'; |           state.status = 'error'; | ||||||
|   | |||||||
| @@ -23,9 +23,9 @@ export const config = figue({ | |||||||
|     env: { |     env: { | ||||||
|       doc: 'Application current env', |       doc: 'Application current env', | ||||||
|       format: 'enum', |       format: 'enum', | ||||||
|       values: ['production', 'development', 'test'], |       values: ['production', 'development', 'preview', 'test'], | ||||||
|       default: 'development', |       default: 'development', | ||||||
|       env: 'MODE', |       env: 'VITE_VERCEL_ENV', | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   plausible: { |   plausible: { | ||||||
|   | |||||||
| @@ -1,8 +1,12 @@ | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { NIcon, useThemeVars } from 'naive-ui'; | import { NIcon, useThemeVars } from 'naive-ui'; | ||||||
| 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 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,106 +31,92 @@ 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> | ||||||
|     </template> |     </template> | ||||||
|  |  | ||||||
|     <template #content> |     <template #content> | ||||||
|       <div class="navigation"> |       <div flex items-center justify-center gap-2> | ||||||
|         <n-button |         <c-button | ||||||
|           :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 /> |         <c-button v-if="config.app.env === 'development'" to="/c-lib" circle variant="text" aria-label="UI Lib"> | ||||||
|  |           <icon-mdi:brush-variant text-20px /> | ||||||
|  |         </c-button> | ||||||
|  |  | ||||||
|         <navbar-buttons v-if="!styleStore.isSmallScreen" /> |         <command-palette /> | ||||||
|  |  | ||||||
|  |         <div> | ||||||
|  |           <NavbarButtons v-if="!styleStore.isSmallScreen" /> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|         <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 +125,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 +151,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; | ||||||
| @@ -225,25 +211,4 @@ const tools = computed<ToolCategory[]>(() => [ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| // ::v-deep(.n-menu-item-content-header) { |  | ||||||
| //   overflow: visible !important; |  | ||||||
| //   // overflow-x: hidden !important; |  | ||||||
| // } |  | ||||||
|  |  | ||||||
| .navigation { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
|   flex-direction: row; |  | ||||||
|  |  | ||||||
|   & > *:not(:last-child) { |  | ||||||
|     margin-right: 5px; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .search-bar { |  | ||||||
|     // width: 100%; |  | ||||||
|     flex-grow: 1; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -2,10 +2,10 @@ | |||||||
| import { useRoute } from 'vue-router'; | 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 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(); | ||||||
|  |  | ||||||
| @@ -23,26 +23,31 @@ const head = computed<HeadObject>(() => ({ | |||||||
|   ], |   ], | ||||||
| })); | })); | ||||||
| useHead(head); | useHead(head); | ||||||
|  | const { t } = useI18n(); | ||||||
|  |  | ||||||
|  | const i18nKey = computed<string>(() => route.path.trim().replace('/', '')); | ||||||
|  | const toolTitle = computed<string>(() => t(`tools.${i18nKey.value}.title`, String(route.meta.name))); | ||||||
|  | const toolDescription = computed<string>(() => t(`tools.${i18nKey.value}.description`, String(route.meta.description))); | ||||||
| </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 }} |             {{ toolTitle }} | ||||||
|           </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" /> | ||||||
|  |  | ||||||
|         <div class="description"> |         <div class="description"> | ||||||
|           {{ route.meta.description }} |           {{ toolDescription }} | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| @@ -50,7 +55,7 @@ useHead(head); | |||||||
|     <div class="tool-content"> |     <div class="tool-content"> | ||||||
|       <slot /> |       <slot /> | ||||||
|     </div> |     </div> | ||||||
|   </base-layout> |   </BaseLayout> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
|   | |||||||
| @@ -1,23 +1,25 @@ | |||||||
| 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'; | ||||||
|  | import { i18nPlugin } from './plugins/i18n.plugin'; | ||||||
|  |  | ||||||
|  | registerSW(); | ||||||
|  |  | ||||||
| const app = createApp(App); | const app = createApp(App); | ||||||
|  |  | ||||||
| app.use(createPinia()); | app.use(createPinia()); | ||||||
| app.use(createHead()); | app.use(createHead()); | ||||||
|  | app.use(i18nPlugin); | ||||||
| app.use(router); | app.use(router); | ||||||
| app.use(naive); | app.use(naive); | ||||||
| app.use(plausible); | app.use(plausible); | ||||||
|   | |||||||
							
								
								
									
										82
									
								
								src/modules/command-palette/command-palette.store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/modules/command-palette/command-palette.store.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | import { defineStore } from 'pinia'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import type { PaletteOption } from './command-palette.types'; | ||||||
|  | import { useToolStore } from '@/tools/tools.store'; | ||||||
|  | import { useFuzzySearch } from '@/composable/fuzzySearch'; | ||||||
|  | import { useStyleStore } from '@/stores/style.store'; | ||||||
|  |  | ||||||
|  | import SunIcon from '~icons/mdi/white-balance-sunny'; | ||||||
|  | import GithubIcon from '~icons/mdi/github'; | ||||||
|  | import BugIcon from '~icons/mdi/bug-outline'; | ||||||
|  | import DiceIcon from '~icons/mdi/dice-5'; | ||||||
|  |  | ||||||
|  | export const useCommandPaletteStore = defineStore('command-palette', () => { | ||||||
|  |   const toolStore = useToolStore(); | ||||||
|  |   const styleStore = useStyleStore(); | ||||||
|  |   const router = useRouter(); | ||||||
|  |   const searchPrompt = ref(''); | ||||||
|  |  | ||||||
|  |   const toolsOptions = toolStore.tools.map(tool => ({ | ||||||
|  |     ...tool, | ||||||
|  |     to: tool.path, | ||||||
|  |     toolCategory: tool.category, | ||||||
|  |     category: 'Tools', | ||||||
|  |   })); | ||||||
|  |  | ||||||
|  |   const searchOptions: PaletteOption[] = [ | ||||||
|  |     ...toolsOptions, | ||||||
|  |     { | ||||||
|  |       name: 'Random tool', | ||||||
|  |       description: 'Get a random tool from the list.', | ||||||
|  |       action: () => { | ||||||
|  |         const { path } = _.sample(toolStore.tools)!; | ||||||
|  |         router.push(path); | ||||||
|  |       }, | ||||||
|  |       icon: DiceIcon, | ||||||
|  |       category: 'Tools', | ||||||
|  |       keywords: ['random', 'tool', 'pick', 'choose', 'select'], | ||||||
|  |       closeOnSelect: true, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       name: 'Toggle dark mode', | ||||||
|  |       description: 'Toggle dark mode on or off.', | ||||||
|  |       action: () => styleStore.toggleDark(), | ||||||
|  |       icon: SunIcon, | ||||||
|  |       category: 'Actions', | ||||||
|  |       keywords: ['dark', 'theme', 'toggle', 'mode', 'light', 'system'], | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       name: 'Github repository', | ||||||
|  |       href: 'https://github.com/CorentinTh/it-tools', | ||||||
|  |       category: 'External', | ||||||
|  |       description: 'View the source code of it-tools on Github.', | ||||||
|  |       keywords: ['github', 'repo', 'repository', 'source', 'code'], | ||||||
|  |       icon: GithubIcon, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       name: 'Report a bug or an issue', | ||||||
|  |       description: 'Report a bug or an issue to help improve it-tools.', | ||||||
|  |       href: 'https://github.com/CorentinTh/it-tools/issues/new/choose', | ||||||
|  |       category: 'Actions', | ||||||
|  |       keywords: ['report', 'issue', 'bug', 'problem', 'error'], | ||||||
|  |       icon: BugIcon, | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   const { searchResult } = useFuzzySearch({ | ||||||
|  |     search: searchPrompt, | ||||||
|  |     data: searchOptions, | ||||||
|  |     options: { | ||||||
|  |       keys: [{ name: 'name', weight: 2 }, 'description', 'keywords', 'category'], | ||||||
|  |       threshold: 0.3, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const filteredSearchResult = computed(() => | ||||||
|  |     _.chain(searchResult.value).groupBy('category').mapValues(categoryOptions => _.take(categoryOptions, 5)).value()); | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     filteredSearchResult, | ||||||
|  |     searchPrompt, | ||||||
|  |   }; | ||||||
|  | }); | ||||||
							
								
								
									
										14
									
								
								src/modules/command-palette/command-palette.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/modules/command-palette/command-palette.types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import type { Component } from 'vue'; | ||||||
|  | import type { RouteLocationRaw } from 'vue-router'; | ||||||
|  |  | ||||||
|  | export interface PaletteOption { | ||||||
|  |   name: string | ||||||
|  |   description?: string | ||||||
|  |   icon?: Component | ||||||
|  |   action?: () => void | ||||||
|  |   to?: RouteLocationRaw | ||||||
|  |   category: string | ||||||
|  |   keywords?: string[] | ||||||
|  |   href?: string | ||||||
|  |   closeOnSelect?: boolean | ||||||
|  | } | ||||||
							
								
								
									
										153
									
								
								src/modules/command-palette/command-palette.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/modules/command-palette/command-palette.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { storeToRefs } from 'pinia'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import { useCommandPaletteStore } from './command-palette.store'; | ||||||
|  | import type { PaletteOption } from './command-palette.types'; | ||||||
|  |  | ||||||
|  | const isModalOpen = ref(false); | ||||||
|  | const inputRef = ref(); | ||||||
|  | const router = useRouter(); | ||||||
|  | const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac')); | ||||||
|  |  | ||||||
|  | const commandPaletteStore = useCommandPaletteStore(); | ||||||
|  | const { searchPrompt, filteredSearchResult } = storeToRefs(commandPaletteStore); | ||||||
|  |  | ||||||
|  | const keys = useMagicKeys({ | ||||||
|  |   passive: false, | ||||||
|  |   onEventFired(e) { | ||||||
|  |     if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') { | ||||||
|  |       e.preventDefault(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (e.metaKey && e.key === 'k' && e.type === 'keydown') { | ||||||
|  |       e.preventDefault(); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | whenever(isModalOpen, () => inputRef.value?.focus()); | ||||||
|  |  | ||||||
|  | whenever(keys.ctrl_k, open); | ||||||
|  | whenever(keys.meta_k, open); | ||||||
|  | whenever(keys.escape, close); | ||||||
|  |  | ||||||
|  | function open() { | ||||||
|  |   return isModalOpen.value = true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function close() { | ||||||
|  |   isModalOpen.value = false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const selectedOptionIndex = ref(0); | ||||||
|  |  | ||||||
|  | function handleKeydown(event: KeyboardEvent) { | ||||||
|  |   const { key } = event; | ||||||
|  |   const isEnterPressed = key === 'Enter'; | ||||||
|  |   const isArrowUpOrDown = ['ArrowUp', 'ArrowDown'].includes(key); | ||||||
|  |   const isArrowDown = key === 'ArrowDown'; | ||||||
|  |  | ||||||
|  |   if (isArrowUpOrDown) { | ||||||
|  |     const increment = isArrowDown ? 1 : -1; | ||||||
|  |     const maxIndex = Math.max(_.chain(filteredSearchResult.value).values().flatten().size().value() - 1, 0); | ||||||
|  |  | ||||||
|  |     selectedOptionIndex.value = Math.min(Math.max(selectedOptionIndex.value + increment, 0), maxIndex); | ||||||
|  |  | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (isEnterPressed) { | ||||||
|  |     const option = _.chain(filteredSearchResult.value) | ||||||
|  |       .values() | ||||||
|  |       .flatten() | ||||||
|  |       .nth(selectedOptionIndex.value) | ||||||
|  |       .value(); | ||||||
|  |  | ||||||
|  |     activateOption(option); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getOptionIndex(option: PaletteOption) { | ||||||
|  |   return _.chain(filteredSearchResult.value) | ||||||
|  |     .values() | ||||||
|  |     .flatten() | ||||||
|  |     .findIndex(o => o === option) | ||||||
|  |     .value(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function activateOption(option: PaletteOption) { | ||||||
|  |   const { closeOnSelect } = option; | ||||||
|  |  | ||||||
|  |   if (option.action) { | ||||||
|  |     option.action(); | ||||||
|  |  | ||||||
|  |     if (closeOnSelect) { | ||||||
|  |       close(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const closeAfterNavigation = closeOnSelect || _.isUndefined(closeOnSelect); | ||||||
|  |  | ||||||
|  |   if (option.to) { | ||||||
|  |     router.push(option.to); | ||||||
|  |  | ||||||
|  |     if (closeAfterNavigation) { | ||||||
|  |       close(); | ||||||
|  |     } | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (option.href) { | ||||||
|  |     window.open(option.href, '_blank'); | ||||||
|  |  | ||||||
|  |     if (closeAfterNavigation) { | ||||||
|  |       close(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <div flex-1> | ||||||
|  |     <c-button w-full important:justify-start @click="isModalOpen = true"> | ||||||
|  |       <span flex items-center gap-3 op-40> | ||||||
|  |  | ||||||
|  |         <icon-mdi-search /> | ||||||
|  |         Search... | ||||||
|  |  | ||||||
|  |         <span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline> | ||||||
|  |           {{ isMac ? 'Cmd' : 'Ctrl' }} + K | ||||||
|  |         </span> | ||||||
|  |       </span> | ||||||
|  |     </c-button> | ||||||
|  |  | ||||||
|  |     <c-modal v-model:open="isModalOpen" class="palette-modal" shadow-xl important:max-w-650px important:pa-12px @keydown="handleKeydown"> | ||||||
|  |       <c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable /> | ||||||
|  |  | ||||||
|  |       <div v-for="(options, category) in filteredSearchResult" :key="category"> | ||||||
|  |         <div ml-3 mt-3 text-sm font-bold text-primary op-60> | ||||||
|  |           {{ category }} | ||||||
|  |         </div> | ||||||
|  |         <command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" /> | ||||||
|  |       </div> | ||||||
|  |     </c-modal> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <style scoped lang="less"> | ||||||
|  | .c-input-text { | ||||||
|  |   font-size: 18px; | ||||||
|  |  | ||||||
|  |   ::v-deep(.input-wrapper) { | ||||||
|  |       padding: 4px; | ||||||
|  |       padding-left: 18px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .c-modal--overlay { | ||||||
|  |   align-items: flex-start !important; | ||||||
|  |   padding-top: 80px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import type { PaletteOption } from '../command-palette.types'; | ||||||
|  |  | ||||||
|  | const props = withDefaults(defineProps<{ option: PaletteOption; selected?: boolean }>(), { | ||||||
|  |   selected: false, | ||||||
|  | }); | ||||||
|  | const emit = defineEmits(['activated']); | ||||||
|  | const { option } = toRefs(props); | ||||||
|  |  | ||||||
|  | const { selected } = toRefs(props); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <div | ||||||
|  |     role="option" | ||||||
|  |     :aria-selected="selected" | ||||||
|  |     :class="{ | ||||||
|  |       'text-white': selected, | ||||||
|  |       'bg-primary': selected, | ||||||
|  |     }" | ||||||
|  |     w-full flex cursor-pointer items-center overflow-hidden rounded pa-3 transition hover:bg-primary hover:text-white | ||||||
|  |     @click="() => emit('activated', option)" | ||||||
|  |   > | ||||||
|  |     <component :is="option.icon" v-if="option.icon" mr-3 h-30px w-30px shrink-0 op-50 /> | ||||||
|  |  | ||||||
|  |     <div flex-1 overflow-hidden> | ||||||
|  |       <div truncate font-bold lh-tight op-90> | ||||||
|  |         {{ option.name }} | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div v-if="option.description" truncate lh-tight op-60> | ||||||
|  |         {{ option.description }} | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
							
								
								
									
										7
									
								
								src/modules/shared/date.models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/modules/shared/date.models.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import { format } from 'date-fns'; | ||||||
|  |  | ||||||
|  | export { getUrlFriendlyDateTime }; | ||||||
|  |  | ||||||
|  | function getUrlFriendlyDateTime({ date = new Date() }: { date?: Date } = {}) { | ||||||
|  |   return format(date, 'yyyy-MM-dd-HH-mm-ss'); | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								src/modules/shared/number.models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/modules/shared/number.models.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | function clamp({ value, min = 0, max = 100 }: { value: number; min?: number; max?: number }) { | ||||||
|  |   return Math.min(Math.max(value, min), max); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { clamp }; | ||||||
| @@ -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 }); | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { Coffee } from '@vicons/tabler'; |  | ||||||
| import { useHead } from '@vueuse/head'; | import { useHead } from '@vueuse/head'; | ||||||
|  |  | ||||||
| useHead({ title: 'Page not found - IT Tools' }); | useHead({ title: 'Page not found - IT Tools' }); | ||||||
| @@ -7,14 +6,22 @@ useHead({ title: 'Page not found - IT Tools' }); | |||||||
|  |  | ||||||
| <template> | <template> | ||||||
|   <div mt-20 flex flex-col items-center> |   <div mt-20 flex flex-col items-center> | ||||||
|     <n-icon :component="Coffee" size="100" depth="3" /> |     <span text-90px lh-1 op-50> | ||||||
|  |       <icon-mdi:kettle-steam-outline /> | ||||||
|  |     </span> | ||||||
|  |  | ||||||
|     <n-h1 m-0 mt-3>404 Not Found</n-h1> |     <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> |     </h1> | ||||||
|  |     <div mt-4 op-60> | ||||||
|  |       Sorry, this page does not seem to exist | ||||||
|  |     </div> | ||||||
|  |     <div mb-8 op-60> | ||||||
|  |       Maybe the cache is doing tricky things, try force-refreshing? | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|     <router-link to="/" #="{ navigate, href }" custom> |     <c-button to="/"> | ||||||
|       <n-button tag="a" :href="href" secondary @click="navigate"> Back home </n-button> |       Back home | ||||||
|     </router-link> |     </c-button> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useTracker } from '@/modules/tracker/tracker.services'; |  | ||||||
| import { useHead } from '@vueuse/head'; | import { useHead } from '@vueuse/head'; | ||||||
|  | import { useTracker } from '@/modules/tracker/tracker.services'; | ||||||
|  |  | ||||||
| useHead({ title: 'About - IT Tools' }); | useHead({ title: 'About - IT Tools' }); | ||||||
| const { tracker } = useTracker(); | const { tracker } = useTracker(); | ||||||
| @@ -11,73 +11,58 @@ 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 feel free to share | ||||||
|  |       it to people you think may find it useful too and don't forget to bookmark 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 costs 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> | ||||||
|  |  | ||||||
|     <n-h2>Found a bug? A tool is missing?</n-h2> |     <n-h2>Found a bug? A tool is missing?</n-h2> | ||||||
|     <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 useful, you are welcome to submit a | ||||||
|       feature request in the |       feature request in the | ||||||
|       <n-button |       <c-link | ||||||
|         type="primary" |         href="https://github.com/CorentinTh/it-tools/issues/new/choose" | ||||||
|         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" |  | ||||||
|         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 doesn't work as expected, please file a bug report in the | ||||||
|       <n-button |       <c-link | ||||||
|         type="primary" |         href="https://github.com/CorentinTh/it-tools/issues/new/choose" | ||||||
|         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" |  | ||||||
|         rel="noopener" |         rel="noopener" | ||||||
|         target="_blank" |         target="_blank" | ||||||
|       > |       > | ||||||
|         issues section |         issues section | ||||||
|       </n-button> |       </c-link> | ||||||
|       in the GitHub repository. |       in the GitHub repository. | ||||||
|     </n-p> |     </n-p> | ||||||
|   </div> |   </div> | ||||||
|   | |||||||
| @@ -1,10 +1,6 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { config } from '@/config'; |  | ||||||
| import { useToolStore } from '@/tools/tools.store'; |  | ||||||
| import { Heart } from '@vicons/tabler'; |  | ||||||
| import { useHead } from '@vueuse/head'; | import { useHead } from '@vueuse/head'; | ||||||
| import ColoredCard from '../components/ColoredCard.vue'; | import { useToolStore } from '@/tools/tools.store'; | ||||||
| import ToolCard from '../components/ToolCard.vue'; |  | ||||||
|  |  | ||||||
| const toolStore = useToolStore(); | const toolStore = useToolStore(); | ||||||
|  |  | ||||||
| @@ -12,89 +8,32 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <template> | <template> | ||||||
|   <div class="home-page"> |   <div class="home-page" m-auto mt-50px max-w-1800px> | ||||||
|     <div class="grid-wrapper"> |     <div my-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> |  | ||||||
|           <colored-card title="You like it-tools?" :icon="Heart"> |  | ||||||
|             Give us a star on |  | ||||||
|             <a |  | ||||||
|               href="https://github.com/CorentinTh/it-tools" |  | ||||||
|               rel="noopener" |  | ||||||
|               target="_blank" |  | ||||||
|               aria-label="IT-Tools' GitHub repository" |  | ||||||
|               >GitHub</a |  | ||||||
|             > |  | ||||||
|             or follow us on |  | ||||||
|             <a |  | ||||||
|               href="https://twitter.com/ittoolsdottech" |  | ||||||
|               rel="noopener" |  | ||||||
|               target="_blank" |  | ||||||
|               aria-label="IT-Tools' Twitter account" |  | ||||||
|               >Twitter</a |  | ||||||
|             >! Thank you |  | ||||||
|             <n-icon :component="Heart" /> |  | ||||||
|           </colored-card> |  | ||||||
|         </n-gi> |  | ||||||
|       </n-grid> |  | ||||||
|  |  | ||||||
|       <transition name="height"> |  | ||||||
|     <div v-if="toolStore.favoriteTools.length > 0"> |     <div v-if="toolStore.favoriteTools.length > 0"> | ||||||
|           <n-h3>Your favorite tools</n-h3> |       <div mb-2 mt-6 text-lg font-semibold> | ||||||
|           <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> |         {{ $t('home.categories.yourFavoriteTools') }} | ||||||
|             <n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name"> |       </div> | ||||||
|               <tool-card :tool="tool" /> |       <div grid-cols="1 sm:2 md:2 lg:3 xl:4" grid gap-12px> | ||||||
|             </n-gi> |         <tool-card v-for="tool in toolStore.favoriteTools" :key="tool.name" :tool="tool" /> | ||||||
|           </n-grid> |       </div> | ||||||
|     </div> |     </div> | ||||||
|       </transition> |  | ||||||
|  |  | ||||||
|     <div v-if="toolStore.newTools.length > 0"> |     <div v-if="toolStore.newTools.length > 0"> | ||||||
|         <n-h3>Newest tools</n-h3> |       <div mb-2 mt-6 text-lg font-semibold> | ||||||
|         <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> |         {{ $t('home.categories.newestTools') }} | ||||||
|           <n-gi v-for="tool in toolStore.newTools" :key="tool.name"> |       </div> | ||||||
|             <tool-card :tool="tool" /> |       <div grid-cols="1 sm:2 md:2 lg:3 xl:4" grid gap-12px> | ||||||
|           </n-gi> |         <tool-card v-for="tool in toolStore.newTools" :key="tool.name" :tool="tool" /> | ||||||
|         </n-grid> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|       <n-h3>All the tools</n-h3> |     <div mb-2 mt-6 text-lg font-semibold> | ||||||
|       <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> |       {{ $t('home.categories.allTheTools') }} | ||||||
|         <n-gi v-for="tool in toolStore.tools" :key="tool.name"> |     </div> | ||||||
|           <transition> |     <div grid-cols="1 sm:2 md:2 lg:3 xl:4" grid gap-12px> | ||||||
|             <tool-card :tool="tool" /> |       <tool-card v-for="tool in toolStore.tools" :key="tool.name" :tool="tool" /> | ||||||
|           </transition> |  | ||||||
|         </n-gi> |  | ||||||
|       </n-grid> |  | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <style scoped lang="less"> |  | ||||||
| .home-page { |  | ||||||
|   padding-top: 50px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .n-h3 { |  | ||||||
|   margin-bottom: 10px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| ::v-deep(.n-grid) { |  | ||||||
|   margin-bottom: 30px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .height-enter-active, |  | ||||||
| .height-leave-active { |  | ||||||
|   transition: all 0.5s ease-in-out; |  | ||||||
|   overflow: hidden; |  | ||||||
|   max-height: 500px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .height-enter-from, |  | ||||||
| .height-leave-to { |  | ||||||
|   max-height: 42px; |  | ||||||
|   overflow: hidden; |  | ||||||
|   opacity: 0; |  | ||||||
|   margin-bottom: 0; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								src/plugins/i18n.plugin.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/plugins/i18n.plugin.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import type { Plugin } from 'vue'; | ||||||
|  | import { createI18n } from 'vue-i18n'; | ||||||
|  | import baseMessages from '@intlify/unplugin-vue-i18n/messages'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import { parse as parseYaml } from 'yaml'; | ||||||
|  |  | ||||||
|  | const i18nFiles = import.meta.glob('../tools/*/locales/**.yml', { as: 'raw' }); | ||||||
|  |  | ||||||
|  | const messagesByTools = await Promise.all(_.map(i18nFiles, async (fileDescriptor, path) => { | ||||||
|  |   const [, locale] = path.match(/\.\/tools\/.*?\/locales\/(.*)\.ya?ml$/i) ?? []; | ||||||
|  |   const content = parseYaml(await fileDescriptor()); | ||||||
|  |  | ||||||
|  |   return { [locale]: content }; | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | const messages = _.merge( | ||||||
|  |   baseMessages, | ||||||
|  |   _.merge({}, ...messagesByTools), | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | const i18n = createI18n({ | ||||||
|  |   legacy: false, | ||||||
|  |   locale: 'en', | ||||||
|  |   messages, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export const i18nPlugin: Plugin = { | ||||||
|  |   install: (app) => { | ||||||
|  |     app.use(i18n); | ||||||
|  |   }, | ||||||
|  | }; | ||||||
| @@ -1,8 +1,8 @@ | |||||||
| import { config } from '@/config'; |  | ||||||
| import { noop } from 'lodash'; | import { noop } from 'lodash'; | ||||||
|  |  | ||||||
| import Plausible from 'plausible-tracker'; | import Plausible from 'plausible-tracker'; | ||||||
| import type { App } from 'vue'; | import type { App } from 'vue'; | ||||||
|  | import { config } from '@/config'; | ||||||
|  |  | ||||||
| function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> { | function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> { | ||||||
|   return { |   return { | ||||||
| @@ -15,11 +15,11 @@ function createPlausibleInstance({ | |||||||
|   config, |   config, | ||||||
| }: { | }: { | ||||||
|   config: { |   config: { | ||||||
|     isTrackerEnabled: boolean; |     isTrackerEnabled: boolean | ||||||
|     domain: string; |     domain: string | ||||||
|     apiHost: string; |     apiHost: string | ||||||
|     trackLocalhost: boolean; |     trackLocalhost: boolean | ||||||
|   }; |   } | ||||||
| }) { | }) { | ||||||
|   if (config.isTrackerEnabled) { |   if (config.isTrackerEnabled) { | ||||||
|     return Plausible(config); |     return Plausible(config); | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import HomePage from './pages/Home.page.vue'; | |||||||
| import NotFound from './pages/404.page.vue'; | import NotFound from './pages/404.page.vue'; | ||||||
| import { tools } from './tools'; | import { tools } from './tools'; | ||||||
| import { config } from './config'; | import { config } from './config'; | ||||||
|  | import { routes as demoRoutes } from './ui/demo/demo.routes'; | ||||||
|  |  | ||||||
| const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({ | const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({ | ||||||
|   path, |   path, | ||||||
| @@ -14,7 +15,7 @@ const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({ | |||||||
| const toolsRedirectRoutes = tools | const toolsRedirectRoutes = tools | ||||||
|   .filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0) |   .filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0) | ||||||
|   .flatMap( |   .flatMap( | ||||||
|     ({ path, redirectFrom }) => redirectFrom?.map((redirectSource) => ({ path: redirectSource, redirect: path })) ?? [], |     ({ path, redirectFrom }) => redirectFrom?.map(redirectSource => ({ path: redirectSource, redirect: path })) ?? [], | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
| const router = createRouter({ | const router = createRouter({ | ||||||
| @@ -32,6 +33,7 @@ const router = createRouter({ | |||||||
|     }, |     }, | ||||||
|     ...toolsRoutes, |     ...toolsRoutes, | ||||||
|     ...toolsRedirectRoutes, |     ...toolsRedirectRoutes, | ||||||
|  |     ...(config.app.env === 'development' ? demoRoutes : []), | ||||||
|     { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }, |     { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }, | ||||||
|   ], |   ], | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								src/shims.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								src/shims.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,10 +1,35 @@ | |||||||
| declare module '*.vue' { | declare module '*.vue' { | ||||||
|   import type { ComponentOptions, ComponentOptions } from 'vue'; |   import type {  ComponentOptions } from 'vue'; | ||||||
|   const Component: ComponentOptions; |   const Component: ComponentOptions; | ||||||
|   export default Component; |   export default Component; | ||||||
| } | } | ||||||
|  |  | ||||||
| declare module '*.md' { | declare module '*.md' { | ||||||
|  |   import type {  ComponentOptions } from 'vue'; | ||||||
|   const Component: ComponentOptions; |   const Component: ComponentOptions; | ||||||
|   export default Component; |   export default Component; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | declare module 'iarna-toml-esm' { | ||||||
|  |   export const parse: (toml: string) => any; | ||||||
|  |   export const stringify: (obj: any) => string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare module 'emojilib' { | ||||||
|  |   const lib: Record<string, string[]>; | ||||||
|  |   export default lib; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare module 'unicode-emoji-json' { | ||||||
|  |   const emoji: Record<string, { | ||||||
|  |     name: string; | ||||||
|  |     slug: string; | ||||||
|  |     group: string; | ||||||
|  |     emoji_version: string; | ||||||
|  |     unicode_version: string; | ||||||
|  |     skin_tone_support: boolean; | ||||||
|  |     skin_tone_support_unicode_version: string; | ||||||
|  |   }>; | ||||||
|  |    | ||||||
|  |   export default emoji; | ||||||
|  | } | ||||||
| @@ -1,17 +1,19 @@ | |||||||
| import { useMediaQuery, useStorage } from '@vueuse/core'; | import { useDark, useMediaQuery, useStorage, useToggle } 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: () => { | ||||||
|     const isDarkTheme = useStorage('isDarkTheme', true) as Ref<boolean>; |     const isDarkTheme = useDark(); | ||||||
|  |     const toggleDark = useToggle(isDarkTheme); | ||||||
|     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, | ||||||
|  |       toggleDark, | ||||||
|       isMenuCollapsed, |       isMenuCollapsed, | ||||||
|       isSmallScreen, |       isSmallScreen, | ||||||
|     }; |     }; | ||||||
|   | |||||||
| @@ -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 } 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> | ||||||
|  |         <div op-60> | ||||||
|  |           Click or drag a file to this area to upload | ||||||
|  |         </div> | ||||||
|  |       </n-upload-dragger> | ||||||
|  |     </n-upload> | ||||||
|  |  | ||||||
|  |     <c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 /> | ||||||
|  |  | ||||||
|  |     <div flex justify-center> | ||||||
|  |       <c-button @click="copyFileBase64()"> | ||||||
|  |         Copy | ||||||
|  |       </c-button> | ||||||
|  |     </div> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <style lang="less" scoped> | ||||||
| ::v-deep(.n-upload-trigger) { | ::v-deep(.n-upload-trigger) { | ||||||
|   width: 100%; |   width: 100%; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import { defineTool } from '../tool'; | |||||||
| export const tool = defineTool({ | export const tool = defineTool({ | ||||||
|   name: 'Base64 file converter', |   name: 'Base64 file converter', | ||||||
|   path: '/base64-file-converter', |   path: '/base64-file-converter', | ||||||
|   description: "Convert string, files or images into a it's base64 representation.", |   description: 'Convert string, files or images into a it\'s base64 representation.', | ||||||
|   keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'], |   keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'], | ||||||
|   component: () => import('./base64-file-converter.vue'), |   component: () => import('./base64-file-converter.vue'), | ||||||
|   icon: FileDigit, |   icon: FileDigit, | ||||||
|   | |||||||
| @@ -1,55 +1,90 @@ | |||||||
| <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 { useCopy } from '@/composable/copy'; | import { useCopy } from '@/composable/copy'; | ||||||
| import { useValidation } from '@/composable/validation'; |  | ||||||
| import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; | import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
|  | const encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false); | ||||||
|  | const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false); | ||||||
|  |  | ||||||
| const textInput = ref(''); | const textInput = ref(''); | ||||||
| const base64Output = computed(() => textToBase64(textInput.value)); | const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.value })); | ||||||
| const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' }); | const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' }); | ||||||
|  |  | ||||||
| const base64Input = ref(''); | const base64Input = ref(''); | ||||||
| const textOutput = computed(() => withDefaultOnError(() => base64ToText(base64Input.value.trim()), '')); | const textOutput = computed(() => | ||||||
|  |   withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''), | ||||||
|  | ); | ||||||
| const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); | const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); | ||||||
| const b64Validation = useValidation({ | const b64ValidationRules = [ | ||||||
|   source: base64Input, |   { | ||||||
|   rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }], |     message: 'Invalid base64 string', | ||||||
| }); |     validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }), | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | const b64ValidationWatch = [decodeUrlSafe]; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <c-card title="String to base64"> | ||||||
|  |     <n-form-item label="Encode URL safe" label-placement="left"> | ||||||
|  |       <n-switch v-model:value="encodeUrlSafe" /> | ||||||
|  |     </n-form-item> | ||||||
|  |     <c-input-text | ||||||
|  |       v-model:value="textInput" | ||||||
|  |       multiline | ||||||
|  |       placeholder="Put your string here..." | ||||||
|  |       rows="5" | ||||||
|  |       label="String to encode" | ||||||
|  |       raw-text | ||||||
|  |       mb-5 | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <c-input-text | ||||||
|  |       label="Base64 of string" | ||||||
|  |       :value="base64Output" | ||||||
|  |       multiline | ||||||
|  |       readonly | ||||||
|  |       placeholder="The base64 encoding of your string will be here" | ||||||
|  |       rows="5" | ||||||
|  |       mb-5 | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <div flex justify-center> | ||||||
|  |       <c-button @click="copyTextBase64()"> | ||||||
|  |         Copy base64 | ||||||
|  |       </c-button> | ||||||
|  |     </div> | ||||||
|  |   </c-card> | ||||||
|  |  | ||||||
|  |   <c-card title="Base64 to string"> | ||||||
|  |     <n-form-item label="Decode URL safe" label-placement="left"> | ||||||
|  |       <n-switch v-model:value="decodeUrlSafe" /> | ||||||
|  |     </n-form-item> | ||||||
|  |     <c-input-text | ||||||
|  |       v-model:value="base64Input" | ||||||
|  |       multiline | ||||||
|  |       placeholder="Your base64 string..." | ||||||
|  |       rows="5" | ||||||
|  |       :validation-rules="b64ValidationRules" | ||||||
|  |       :validation-watch="b64ValidationWatch" | ||||||
|  |       label="Base64 string to decode" | ||||||
|  |       mb-5 | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <c-input-text | ||||||
|  |       v-model:value="textOutput" | ||||||
|  |       label="Decoded string" | ||||||
|  |       placeholder="The decoded string will be here" | ||||||
|  |       multiline | ||||||
|  |       rows="5" | ||||||
|  |       readonly | ||||||
|  |       mb-5 | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <div flex justify-center> | ||||||
|  |       <c-button @click="copyText()"> | ||||||
|  |         Copy decoded string | ||||||
|  |       </c-button> | ||||||
|  |     </div> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|   | |||||||
| @@ -1,37 +1,6 @@ | |||||||
| <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 { 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 +9,34 @@ const header = computed(() => `Authorization: Basic ${textToBase64(`${username.v | |||||||
| const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' }); | const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 /> | ||||||
|  |     <c-input-text | ||||||
|  |       v-model:value="password" | ||||||
|  |       label="Password" | ||||||
|  |       placeholder="Your password..." | ||||||
|  |       clearable | ||||||
|  |       raw-text | ||||||
|  |       mb-2 | ||||||
|  |       type="password" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <c-card> | ||||||
|  |       <n-statistic label="Authorization header:" class="header"> | ||||||
|  |         <n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none"> | ||||||
|  |           {{ header }} | ||||||
|  |         </n-scrollbar> | ||||||
|  |       </n-statistic> | ||||||
|  |     </c-card> | ||||||
|  |     <div mt-5 flex justify-center> | ||||||
|  |       <c-button @click="copy()"> | ||||||
|  |         Copy header | ||||||
|  |       </c-button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| ::v-deep(.n-statistic-value__content) { | ::v-deep(.n-statistic-value__content) { | ||||||
|   font-family: monospace; |   font-family: monospace; | ||||||
|   | |||||||
| @@ -1,63 +1,7 @@ | |||||||
| <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 { compareSync, hashSync } from 'bcryptjs'; | ||||||
| import { hashSync, compareSync } 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 +15,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 hash to compare..." raw-text /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> | ||||||
|  |         <div class="compare-result" :class="{ positive: compareMatch }"> | ||||||
|  |           {{ compareMatch ? 'Yes' : 'No' }} | ||||||
|  |         </div> | ||||||
|  |       </n-form-item> | ||||||
|  |     </n-form> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .compare-result { | .compare-result { | ||||||
|   color: v-bind('themeVars.errorColor'); |   color: v-bind('themeVars.errorColor'); | ||||||
|   | |||||||
| @@ -13,22 +13,22 @@ 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 }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; headerMap?: Record<string, string> }) { | function arrayToMarkdownTable({ data, headerMap = {} }: { data: Record<string, unknown>[]; headerMap?: Record<string, string> }) { | ||||||
|   if (!Array.isArray(data) || data.length === 0) { |   if (!Array.isArray(data) || data.length === 0) { | ||||||
|     return ''; |     return ''; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const headers = Object.keys(data[0]); |   const headers = Object.keys(data[0]); | ||||||
|   const rows = data.map((obj) => Object.values(obj)); |   const rows = data.map(obj => Object.values(obj)); | ||||||
|  |  | ||||||
|   const headerRow = `| ${headers.map((header) => headerMap[header] ?? header).join(' | ')} |`; |   const headerRow = `| ${headers.map(header => headerMap[header] ?? header).join(' | ')} |`; | ||||||
|   const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`; |   const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`; | ||||||
|   const dataRows = rows.map((row) => `| ${row.join(' | ')} |`).join('\n'); |   const dataRows = rows.map(row => `| ${row.join(' | ')} |`).join('\n'); | ||||||
|  |  | ||||||
|   return `${headerRow}\n${separatorRow}\n${dataRows}`; |   return `${headerRow}\n${separatorRow}\n${dataRows}`; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,92 +1,11 @@ | |||||||
| <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 { useStorage } from '@vueuse/core'; | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| 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'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  |  | ||||||
| const suites = useStorage('benchmark-builder:suites', [ | const suites = useStorage('benchmark-builder:suites', [ | ||||||
|   { title: 'Suite 1', data: [5, 10] }, |   { title: 'Suite 1', data: [5, 10] }, | ||||||
| @@ -116,8 +35,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, | ||||||
| @@ -129,7 +48,7 @@ const results = computed(() => { | |||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const { copy } = useClipboard(); | const { copy } = useCopy({ createToast: false }); | ||||||
|  |  | ||||||
| const header = { | const header = { | ||||||
|   title: 'Suite', |   title: 'Suite', | ||||||
| @@ -159,4 +78,87 @@ function copyAsBulletList() { | |||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="less" scoped></style> | <template> | ||||||
|  |   <n-scrollbar style="flex: 1" x-scrollable> | ||||||
|  |     <div mb-5 flex flex-1 flex-nowrap justify-center gap-12px> | ||||||
|  |       <div v-for="(suite, index) of suites" :key="index"> | ||||||
|  |         <c-card style="width: 294px"> | ||||||
|  |           <c-input-text | ||||||
|  |             v-model:value="suite.title" | ||||||
|  |             label-position="left" | ||||||
|  |             label="Suite name" | ||||||
|  |             placeholder="Suite name..." | ||||||
|  |             clearable | ||||||
|  |           /> | ||||||
|  |  | ||||||
|  |           <n-divider /> | ||||||
|  |           <n-form-item label="Suite values" :show-feedback="false"> | ||||||
|  |             <DynamicValues v-model:values="suite.data" /> | ||||||
|  |           </n-form-item> | ||||||
|  |         </c-card> | ||||||
|  |  | ||||||
|  |         <div flex justify-center> | ||||||
|  |           <c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)"> | ||||||
|  |             <n-icon :component="Trash" depth="3" mr-2 size="18" /> | ||||||
|  |             Delete suite | ||||||
|  |           </c-button> | ||||||
|  |           <c-button | ||||||
|  |             variant="text" | ||||||
|  |             @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })" | ||||||
|  |           > | ||||||
|  |             <n-icon :component="Plus" depth="3" mr-2 size="18" /> | ||||||
|  |             Add suite | ||||||
|  |           </c-button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </n-scrollbar> | ||||||
|  |  | ||||||
|  |   <div style="flex: 0 0 100%"> | ||||||
|  |     <div style="max-width: 600px; margin: 0 auto"> | ||||||
|  |       <div mx-auto max-w-sm flex justify-center gap-3> | ||||||
|  |         <c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 /> | ||||||
|  |  | ||||||
|  |         <c-button | ||||||
|  |           @click=" | ||||||
|  |             suites = [ | ||||||
|  |               { title: 'Suite 1', data: [] }, | ||||||
|  |               { title: 'Suite 2', data: [] }, | ||||||
|  |             ] | ||||||
|  |           " | ||||||
|  |         > | ||||||
|  |           Reset suites | ||||||
|  |         </c-button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <n-table> | ||||||
|  |         <thead> | ||||||
|  |           <tr> | ||||||
|  |             <th>{{ header.position }}</th> | ||||||
|  |             <th>{{ header.title }}</th> | ||||||
|  |             <th>{{ header.size }}</th> | ||||||
|  |             <th>{{ header.mean }}</th> | ||||||
|  |             <th>{{ header.variance }}</th> | ||||||
|  |           </tr> | ||||||
|  |         </thead> | ||||||
|  |         <tbody> | ||||||
|  |           <tr v-for="{ title, size, mean, variance, position } of results" :key="title"> | ||||||
|  |             <td>{{ position }}</td> | ||||||
|  |             <td>{{ title }}</td> | ||||||
|  |             <td>{{ size }}</td> | ||||||
|  |             <td>{{ mean }}</td> | ||||||
|  |             <td>{{ variance }}</td> | ||||||
|  |           </tr> | ||||||
|  |         </tbody> | ||||||
|  |       </n-table> | ||||||
|  |       <div mt-5 flex justify-center gap-3> | ||||||
|  |         <c-button @click="copyAsMarkdown()"> | ||||||
|  |           Copy as markdown table | ||||||
|  |         </c-button> | ||||||
|  |         <c-button @click="copyAsBulletList()"> | ||||||
|  |           Copy as bullet list | ||||||
|  |         </c-button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|   | |||||||
| @@ -1,45 +1,15 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <n-space v-for="(value, index) of values" :key="index" :wrap="false" style="margin-bottom: 5px" :size="5"> |  | ||||||
|       <n-input-number |  | ||||||
|         :ref="refs.set" |  | ||||||
|         v-model:value="values[index]" |  | ||||||
|         :show-button="false" |  | ||||||
|         placeholder="Set your measure..." |  | ||||||
|         autofocus |  | ||||||
|         @keydown.enter="onInputEnter(index)" |  | ||||||
|       /> |  | ||||||
|       <n-tooltip> |  | ||||||
|         <template #trigger> |  | ||||||
|           <n-button circle quaternary @click="values.splice(index, 1)"> |  | ||||||
|             <template #icon> |  | ||||||
|               <n-icon :component="Trash" depth="3" /> |  | ||||||
|             </template> |  | ||||||
|           </n-button> |  | ||||||
|         </template> |  | ||||||
|         Delete value |  | ||||||
|       </n-tooltip> |  | ||||||
|     </n-space> |  | ||||||
|  |  | ||||||
|     <n-button tertiary @click="addValue"> |  | ||||||
|       <template #icon> |  | ||||||
|         <n-icon :component="Plus" /> |  | ||||||
|       </template> |  | ||||||
|       Add a measure |  | ||||||
|     </n-button> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { Trash, Plus } from '@vicons/tabler'; | import { Plus, Trash } from '@vicons/tabler'; | ||||||
| import { useTemplateRefsList, useVModel } from '@vueuse/core'; | import { useTemplateRefsList, useVModel } from '@vueuse/core'; | ||||||
| import { NInputNumber } from 'naive-ui'; | import { NInputNumber } from 'naive-ui'; | ||||||
| import { nextTick } from 'vue'; | import { nextTick } from 'vue'; | ||||||
|  |  | ||||||
|  | const props = defineProps<{ values: (number | null)[] }>(); | ||||||
|  |  | ||||||
|  | const emit = defineEmits(['update:values']); | ||||||
|  |  | ||||||
| const refs = useTemplateRefsList<typeof NInputNumber>(); | const refs = useTemplateRefsList<typeof NInputNumber>(); | ||||||
|  |  | ||||||
| const props = defineProps<{ values: (number | null)[] }>(); |  | ||||||
| const emit = defineEmits(['update:values']); |  | ||||||
| const values = useVModel(props, 'values', emit); | const values = useVModel(props, 'values', emit); | ||||||
|  |  | ||||||
| async function addValue() { | async function addValue() { | ||||||
| @@ -58,4 +28,30 @@ function onInputEnter(index: number) { | |||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style scoped></style> | <template> | ||||||
|  |   <div> | ||||||
|  |     <div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2> | ||||||
|  |       <NInputNumber | ||||||
|  |         :ref="refs.set" | ||||||
|  |         v-model:value="values[index]" | ||||||
|  |         :show-button="false" | ||||||
|  |         placeholder="Set your measure..." | ||||||
|  |         autofocus | ||||||
|  |         @keydown.enter="onInputEnter(index)" | ||||||
|  |       /> | ||||||
|  |       <n-tooltip> | ||||||
|  |         <template #trigger> | ||||||
|  |           <c-button circle variant="text" @click="values.splice(index, 1)"> | ||||||
|  |             <n-icon :component="Trash" depth="3" size="18" /> | ||||||
|  |           </c-button> | ||||||
|  |         </template> | ||||||
|  |         Delete value | ||||||
|  |       </n-tooltip> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <c-button @click="addValue"> | ||||||
|  |       <n-icon :component="Plus" depth="3" mr-2 size="18" /> | ||||||
|  |       Add a measure | ||||||
|  |     </c-button> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|   | |||||||
| @@ -1,67 +1,4 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <n-card> |  | ||||||
|       <n-grid cols="3" x-gap="12"> |  | ||||||
|         <n-gi span="1"> |  | ||||||
|           <n-form-item label="Language:"> |  | ||||||
|             <n-select |  | ||||||
|               v-model:value="language" |  | ||||||
|               :options="Object.keys(languages).map((label) => ({ label, value: label }))" |  | ||||||
|             /> |  | ||||||
|           </n-form-item> |  | ||||||
|         </n-gi> |  | ||||||
|         <n-gi span="2"> |  | ||||||
|           <n-form-item |  | ||||||
|             label="Entropy (seed):" |  | ||||||
|             :feedback="entropyValidation.message" |  | ||||||
|             :validation-status="entropyValidation.status" |  | ||||||
|           > |  | ||||||
|             <n-input-group> |  | ||||||
|               <n-input v-model:value="entropy" placeholder="Your string..." /> |  | ||||||
|               <n-button @click="refreshEntropy"> |  | ||||||
|                 <n-icon size="22"> |  | ||||||
|                   <Refresh /> |  | ||||||
|                 </n-icon> |  | ||||||
|               </n-button> |  | ||||||
|               <n-button @click="copyEntropy"> |  | ||||||
|                 <n-icon size="22"> |  | ||||||
|                   <Copy /> |  | ||||||
|                 </n-icon> |  | ||||||
|               </n-button> |  | ||||||
|             </n-input-group> |  | ||||||
|           </n-form-item> |  | ||||||
|         </n-gi> |  | ||||||
|       </n-grid> |  | ||||||
|       <n-form-item |  | ||||||
|         label="Passphrase (mnemonic):" |  | ||||||
|         :feedback="mnemonicValidation.message" |  | ||||||
|         :validation-status="mnemonicValidation.status" |  | ||||||
|       > |  | ||||||
|         <n-input-group> |  | ||||||
|           <n-input |  | ||||||
|             v-model:value="passphrase" |  | ||||||
|             style="text-align: center; flex: 1" |  | ||||||
|             placeholder="Your mnemonic..." |  | ||||||
|             autocomplete="off" |  | ||||||
|             autocorrect="off" |  | ||||||
|             autocapitalize="off" |  | ||||||
|             spellcheck="false" |  | ||||||
|           /> |  | ||||||
|  |  | ||||||
|           <n-button @click="copyPassphrase"> |  | ||||||
|             <n-icon size="22" :component="Copy" /> |  | ||||||
|           </n-button> |  | ||||||
|         </n-input-group> |  | ||||||
|       </n-form-item> |  | ||||||
|     </n-card> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useCopy } from '@/composable/copy'; |  | ||||||
| import { useValidation } from '@/composable/validation'; |  | ||||||
| import { isNotThrowing } from '@/utils/boolean'; |  | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; |  | ||||||
| import { | import { | ||||||
|   chineseSimplifiedWordList, |   chineseSimplifiedWordList, | ||||||
|   chineseTraditionalWordList, |   chineseTraditionalWordList, | ||||||
| @@ -78,19 +15,23 @@ import { | |||||||
|   spanishWordList, |   spanishWordList, | ||||||
| } 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 { 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"> | ||||||
|  |         <c-select | ||||||
|  |           v-model:value="language" | ||||||
|  |           searchable | ||||||
|  |           label="Language:" | ||||||
|  |           :options="Object.keys(languages)" | ||||||
|  |         /> | ||||||
|  |       </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> | ||||||
|   | |||||||
							
								
								
									
										214
									
								
								src/tools/camera-recorder/camera-recorder.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								src/tools/camera-recorder/camera-recorder.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,214 @@ | |||||||
|  | <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 flex-col gap-2> | ||||||
|  |         <c-select | ||||||
|  |           v-model:value="currentCamera" | ||||||
|  |           label-position="left" | ||||||
|  |           label-width="60px" | ||||||
|  |           label="Video:" | ||||||
|  |           :options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))" | ||||||
|  |           placeholder="Select camera" | ||||||
|  |         /> | ||||||
|  |         <c-select | ||||||
|  |           v-if="currentMicrophone && microphones.length > 0" | ||||||
|  |           v-model:value="currentMicrophone" | ||||||
|  |           label="Audio:" | ||||||
|  |           label-position="left" | ||||||
|  |           label-width="60px" | ||||||
|  |           :options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))" | ||||||
|  |           placeholder="Select microphone" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div v-if="!isMediaStreamAvailable" mt-3 flex justify-center> | ||||||
|  |         <c-button type="primary" @click="start"> | ||||||
|  |           Start webcam | ||||||
|  |         </c-button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div v-else> | ||||||
|  |         <div my-2> | ||||||
|  |           <video ref="video" autoplay controls playsinline max-h-full w-full /> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div flex items-center justify-between gap-2> | ||||||
|  |           <c-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot"> | ||||||
|  |             <span mr-2> <icon-mdi-camera /></span> | ||||||
|  |             Take screenshot | ||||||
|  |           </c-button> | ||||||
|  |  | ||||||
|  |           <div v-if="isRecordingSupported" flex justify-center gap-2> | ||||||
|  |             <c-button v-if="recordingState === 'stopped'" @click="startRecording"> | ||||||
|  |               <span mr-2> <icon-mdi-video /></span> | ||||||
|  |               Start recording | ||||||
|  |             </c-button> | ||||||
|  |  | ||||||
|  |             <c-button v-if="recordingState === 'recording'" @click="pauseRecording"> | ||||||
|  |               <span mr-2> <icon-mdi-pause /></span> | ||||||
|  |               Pause | ||||||
|  |             </c-button> | ||||||
|  |  | ||||||
|  |             <c-button v-if="recordingState === 'paused'" @click="resumeRecording"> | ||||||
|  |               <span mr-2> <icon-mdi-play /></span> | ||||||
|  |               Resume | ||||||
|  |             </c-button> | ||||||
|  |  | ||||||
|  |             <c-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording"> | ||||||
|  |               <span mr-2> <icon-mdi-record /></span> | ||||||
|  |               Stop | ||||||
|  |             </c-button> | ||||||
|  |           </div> | ||||||
|  |           <div v-else italic op-60> | ||||||
|  |             Video recording is not supported in your browser | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </c-card> | ||||||
|  |  | ||||||
|  |     <div grid grid-cols-2 mt-5 gap-2> | ||||||
|  |       <c-card v-for="({ type, value, createdAt }, index) in medias" :key="index"> | ||||||
|  |         <img v-if="type === 'image'" :src="value" max-h-full w-full alt="screenshot"> | ||||||
|  |  | ||||||
|  |         <video v-else :src="value" controls max-h-full w-full /> | ||||||
|  |  | ||||||
|  |         <div flex items-center justify-between> | ||||||
|  |           <div font-bold> | ||||||
|  |             {{ type === 'image' ? 'Screenshot' : 'Video' }} | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           <div flex gap-2> | ||||||
|  |             <c-button @click="downloadMedia({ type, value, createdAt })"> | ||||||
|  |               <icon-mdi-download /> | ||||||
|  |             </c-button> | ||||||
|  |  | ||||||
|  |             <c-button @click="medias = medias.filter((_ignored, i) => i !== index)"> | ||||||
|  |               <icon-mdi-delete-outline /> | ||||||
|  |             </c-button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </c-card> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
							
								
								
									
										12
									
								
								src/tools/camera-recorder/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/camera-recorder/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | import { Camera } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  |  | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'Camera recorder', | ||||||
|  |   path: '/camera-recorder', | ||||||
|  |   description: 'Take a picture or record a video from your webcam or camera.', | ||||||
|  |   keywords: ['camera', 'recoder'], | ||||||
|  |   component: () => import('./camera-recorder.vue'), | ||||||
|  |   icon: Camera, | ||||||
|  |   createdAt: new Date('2023-05-15'), | ||||||
|  | }); | ||||||
							
								
								
									
										114
									
								
								src/tools/camera-recorder/useMediaRecorder.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/tools/camera-recorder/useMediaRecorder.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | import { type Ref, computed, ref } from 'vue'; | ||||||
|  |  | ||||||
|  | export { useMediaRecorder }; | ||||||
|  |  | ||||||
|  | function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): { | ||||||
|  |   isRecordingSupported: Ref<boolean> | ||||||
|  |   recordingState: Ref<'stopped' | 'recording' | 'paused'> | ||||||
|  |   startRecording: () => void | ||||||
|  |   stopRecording: () => void | ||||||
|  |   pauseRecording: () => void | ||||||
|  |   resumeRecording: () => void | ||||||
|  |   onRecordAvailable: (cb: (url: string) => void) => void | ||||||
|  | } { | ||||||
|  |   const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm')); | ||||||
|  |   const mediaRecorder = ref<MediaRecorder | null>(null); | ||||||
|  |   const recordedChunks = ref<Blob[]>([]); | ||||||
|  |   const recordAvailable = createEventHook(); | ||||||
|  |   const recordingState = ref<'stopped' | 'recording' | 'paused'>('stopped'); | ||||||
|  |  | ||||||
|  |   const createVideo = () => { | ||||||
|  |     const blob = new Blob(recordedChunks.value, { type: 'video/webm' }); | ||||||
|  |     const url = URL.createObjectURL(blob); | ||||||
|  |     recordedChunks.value = []; | ||||||
|  |     return url; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const startRecording = () => { | ||||||
|  |     if (!isRecordingSupported.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!stream.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (recordingState.value !== 'stopped') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' }); | ||||||
|  |  | ||||||
|  |     mediaRecorder.value.ondataavailable = (e) => { | ||||||
|  |       if (e.data.size > 0) { | ||||||
|  |         recordedChunks.value.push(e.data); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     mediaRecorder.value.onstop = () => { | ||||||
|  |       recordAvailable.trigger(createVideo()); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (mediaRecorder.value.state !== 'inactive') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     mediaRecorder.value.start(); | ||||||
|  |     recordingState.value = 'recording'; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const stopRecording = () => { | ||||||
|  |     if (!isRecordingSupported.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!mediaRecorder.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (recordingState.value === 'stopped') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     mediaRecorder.value.stop(); | ||||||
|  |     recordingState.value = 'stopped'; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const pauseRecording = () => { | ||||||
|  |     if (!isRecordingSupported.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!mediaRecorder.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (recordingState.value !== 'recording') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     mediaRecorder.value.pause(); | ||||||
|  |     recordingState.value = 'paused'; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const resumeRecording = () => { | ||||||
|  |     if (!isRecordingSupported.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!mediaRecorder.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (recordingState.value !== 'paused') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     mediaRecorder.value.resume(); | ||||||
|  |     recordingState.value = 'recording'; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     isRecordingSupported, | ||||||
|  |     startRecording, | ||||||
|  |     stopRecording, | ||||||
|  |     pauseRecording, | ||||||
|  |     resumeRecording, | ||||||
|  |     recordingState, | ||||||
|  |  | ||||||
|  |     onRecordAvailable: recordAvailable.on, | ||||||
|  |   }; | ||||||
|  | } | ||||||
| @@ -1,51 +1,4 @@ | |||||||
| <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 { | import { | ||||||
|   camelCase, |   camelCase, | ||||||
|   capitalCase, |   capitalCase, | ||||||
| @@ -66,10 +19,88 @@ const baseConfig = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const input = ref('lorem ipsum dolor sit amet'); | const input = ref('lorem ipsum dolor sit amet'); | ||||||
|  |  | ||||||
|  | const formats = computed(() => [ | ||||||
|  |   { | ||||||
|  |     label: 'Lowercase:', | ||||||
|  |     value: noCase(input.value, baseConfig).toLocaleLowerCase(), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Uppercase:', | ||||||
|  |     value: noCase(input.value, baseConfig).toLocaleUpperCase(), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Camelcase:', | ||||||
|  |     value: camelCase(input.value, baseConfig), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Capitalcase:', | ||||||
|  |     value: capitalCase(input.value, baseConfig), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Constantcase:', | ||||||
|  |     value: constantCase(input.value, baseConfig), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Dotcase:', | ||||||
|  |     value: dotCase(input.value, baseConfig), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Headercase:', | ||||||
|  |     value: headerCase(input.value, baseConfig), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Nocase:', | ||||||
|  |     value: noCase(input.value, baseConfig), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Paramcase:', | ||||||
|  |     value: paramCase(input.value, baseConfig), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Pascalcase:', | ||||||
|  |     value: pascalCase(input.value, baseConfig), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Pathcase:', | ||||||
|  |     value: pathCase(input.value, baseConfig), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Sentencecase:', | ||||||
|  |     value: sentenceCase(input.value, baseConfig), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Snakecase:', | ||||||
|  |     value: snakeCase(input.value, baseConfig), | ||||||
|  |   }, | ||||||
|  | ]); | ||||||
|  |  | ||||||
|  | const inputLabelAlignmentConfig = { | ||||||
|  |   labelPosition: 'left', | ||||||
|  |   labelWidth: '120px', | ||||||
|  |   labelAlign: 'right', | ||||||
|  | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <template> | ||||||
| .n-form-item { |   <c-card> | ||||||
|   margin: 5px 0; |     <c-input-text | ||||||
| } |       v-model:value="input" | ||||||
| </style> |       label="Your string:" | ||||||
|  |       placeholder="Your string..." | ||||||
|  |       raw-text | ||||||
|  |       v-bind="inputLabelAlignmentConfig" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <div my-16px divider /> | ||||||
|  |  | ||||||
|  |     <InputCopyable | ||||||
|  |       v-for="format in formats" | ||||||
|  |       :key="format.label" | ||||||
|  |       :value="format.value" | ||||||
|  |       :label="format.label" | ||||||
|  |       v-bind="inputLabelAlignmentConfig" | ||||||
|  |       mb-1 | ||||||
|  |     /> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { expect, describe, it } from 'vitest'; | import { describe, expect, it } from 'vitest'; | ||||||
| import { computeChmodOctalRepresentation } from './chmod-calculator.service'; | import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service'; | ||||||
|  |  | ||||||
| describe('chmod-calculator', () => { | describe('chmod-calculator', () => { | ||||||
|   describe('computeChmodOctalRepresentation', () => { |   describe('computeChmodOctalRepresentation', () => { | ||||||
| @@ -64,5 +64,67 @@ describe('chmod-calculator', () => { | |||||||
|         }), |         }), | ||||||
|       ).to.eql('222'); |       ).to.eql('222'); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     it('get the symbolic representation from permissions', () => { | ||||||
|  |       expect( | ||||||
|  |         computeChmodSymbolicRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: true, write: true, execute: true }, | ||||||
|  |             group: { read: true, write: true, execute: true }, | ||||||
|  |             public: { read: true, write: true, execute: true }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('rwxrwxrwx'); | ||||||
|  |  | ||||||
|  |       expect( | ||||||
|  |         computeChmodSymbolicRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: false, write: false, execute: false }, | ||||||
|  |             group: { read: false, write: false, execute: false }, | ||||||
|  |             public: { read: false, write: false, execute: false }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('---------'); | ||||||
|  |  | ||||||
|  |       expect( | ||||||
|  |         computeChmodSymbolicRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: false, write: true, execute: false }, | ||||||
|  |             group: { read: false, write: true, execute: true }, | ||||||
|  |             public: { read: true, write: false, execute: true }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('-w--wxr-x'); | ||||||
|  |  | ||||||
|  |       expect( | ||||||
|  |         computeChmodSymbolicRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: true, write: false, execute: false }, | ||||||
|  |             group: { read: false, write: true, execute: false }, | ||||||
|  |             public: { read: false, write: false, execute: true }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('r---w---x'); | ||||||
|  |  | ||||||
|  |       expect( | ||||||
|  |         computeChmodSymbolicRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: false, write: false, execute: true }, | ||||||
|  |             group: { read: false, write: true, execute: false }, | ||||||
|  |             public: { read: true, write: false, execute: false }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('--x-w-r--'); | ||||||
|  |  | ||||||
|  |       expect( | ||||||
|  |         computeChmodSymbolicRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: false, write: true, execute: false }, | ||||||
|  |             group: { read: false, write: true, execute: false }, | ||||||
|  |             public: { read: false, write: true, execute: false }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('-w--w--w-'); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| import type { GroupPermissions, Permissions } from './chmod-calculator.types'; | import type { GroupPermissions, Permissions } from './chmod-calculator.types'; | ||||||
|  |  | ||||||
| export { computeChmodOctalRepresentation }; | export { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation }; | ||||||
|  |  | ||||||
| function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string { | function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string { | ||||||
|   const permissionValue = { read: 4, write: 2, execute: 1 }; |   const permissionValue = { read: 4, write: 2, execute: 1 }; | ||||||
| @@ -15,3 +15,16 @@ function computeChmodOctalRepresentation({ permissions }: { permissions: Permiss | |||||||
|     getGroupPermissionValue(permissions.public), |     getGroupPermissionValue(permissions.public), | ||||||
|   ].join(''); |   ].join(''); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function computeChmodSymbolicRepresentation({ permissions }: { permissions: Permissions }): string { | ||||||
|  |   const permissionValue = { read: 'r', write: 'w', execute: 'x' }; | ||||||
|  |  | ||||||
|  |   const getGroupPermissionValue = (permission: GroupPermissions) => | ||||||
|  |     _.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, '') : '-'), ''); | ||||||
|  |  | ||||||
|  |   return [ | ||||||
|  |     getGroupPermissionValue(permissions.owner), | ||||||
|  |     getGroupPermissionValue(permissions.group), | ||||||
|  |     getGroupPermissionValue(permissions.public), | ||||||
|  |   ].join(''); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -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 { computeChmodOctalRepresentation } from './chmod-calculator.service'; |  | ||||||
| import InputCopyable from '../../components/InputCopyable.vue'; | import InputCopyable from '../../components/InputCopyable.vue'; | ||||||
|  | import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service'; | ||||||
|  |  | ||||||
| import type { Group, Scope } from './chmod-calculator.types'; | import type { Group, Scope } from './chmod-calculator.types'; | ||||||
|  |  | ||||||
| @@ -52,8 +22,50 @@ const permissions = ref({ | |||||||
| }); | }); | ||||||
|  |  | ||||||
| const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value })); | const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value })); | ||||||
|  | const symbolic = computed(() => computeChmodSymbolicRepresentation({ 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> | ||||||
|  |     <div class="octal-result"> | ||||||
|  |       {{ symbolic }} | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <InputCopyable :value="`chmod ${octal} path`" readonly /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .octal-result { | .octal-result { | ||||||
|   text-align: center; |   text-align: center; | ||||||
|   | |||||||
| @@ -1,21 +1,6 @@ | |||||||
| <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 { formatMs } from './chronometer.service'; | import { formatMs } from './chronometer.service'; | ||||||
|  |  | ||||||
| const isRunning = ref(false); | const isRunning = ref(false); | ||||||
| @@ -43,6 +28,28 @@ function pause() { | |||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-card> | ||||||
|  |       <div class="duration"> | ||||||
|  |         {{ formatMs(counter) }} | ||||||
|  |       </div> | ||||||
|  |     </c-card> | ||||||
|  |     <div mt-5 flex justify-center gap-3> | ||||||
|  |       <c-button v-if="!isRunning" type="primary" @click="resume"> | ||||||
|  |         Start | ||||||
|  |       </c-button> | ||||||
|  |       <c-button v-else type="warning" @click="pause"> | ||||||
|  |         Stop | ||||||
|  |       </c-button> | ||||||
|  |  | ||||||
|  |       <c-button @click="counter = 0"> | ||||||
|  |         Reset | ||||||
|  |       </c-button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .duration { | .duration { | ||||||
|   text-align: center; |   text-align: center; | ||||||
|   | |||||||
| @@ -1,40 +1,4 @@ | |||||||
| <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 { colord, extend } from 'colord'; | import { colord, extend } from 'colord'; | ||||||
|  |  | ||||||
| import cmykPlugin from 'colord/plugins/cmyk'; | import cmykPlugin from 'colord/plugins/cmyk'; | ||||||
| @@ -54,16 +18,70 @@ const cmyk = ref(''); | |||||||
| const lch = ref(''); | const lch = ref(''); | ||||||
|  |  | ||||||
| function onInputUpdated(value: string, omit: string) { | function onInputUpdated(value: string, omit: string) { | ||||||
|  |   try { | ||||||
|     const color = colord(value); |     const color = colord(value); | ||||||
|  |  | ||||||
|   if (omit !== 'name') name.value = color.toName({ closest: true }) ?? ''; |     if (omit !== 'name') { | ||||||
|   if (omit !== 'hex') hex.value = color.toHex(); |       name.value = color.toName({ closest: true }) ?? ''; | ||||||
|   if (omit !== 'rgb') rgb.value = color.toRgbString(); |     } | ||||||
|   if (omit !== 'hsl') hsl.value = color.toHslString(); |     if (omit !== 'hex') { | ||||||
|   if (omit !== 'hwb') hwb.value = color.toHwbString(); |       hex.value = color.toHex(); | ||||||
|   if (omit !== 'cmyk') cmyk.value = color.toCmykString(); |     } | ||||||
|   if (omit !== 'lch') lch.value = color.toLchString(); |     if (omit !== 'rgb') { | ||||||
|  |       rgb.value = color.toRgbString(); | ||||||
|  |     } | ||||||
|  |     if (omit !== 'hsl') { | ||||||
|  |       hsl.value = color.toHslString(); | ||||||
|  |     } | ||||||
|  |     if (omit !== 'hwb') { | ||||||
|  |       hwb.value = color.toHwbString(); | ||||||
|  |     } | ||||||
|  |     if (omit !== 'cmyk') { | ||||||
|  |       cmyk.value = color.toCmykString(); | ||||||
|  |     } | ||||||
|  |     if (omit !== 'lch') { | ||||||
|  |       lch.value = color.toLchString(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   catch { | ||||||
|  |     // | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| onInputUpdated(hex.value, 'hex'); | onInputUpdated(hex.value, 'hex'); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <c-card> | ||||||
|  |     <n-form label-width="100" label-placement="left"> | ||||||
|  |       <n-form-item label="color picker:"> | ||||||
|  |         <n-color-picker | ||||||
|  |           v-model:value="hex" | ||||||
|  |           placement="bottom-end" | ||||||
|  |           @update:value="(v: string) => onInputUpdated(v, 'hex')" | ||||||
|  |         /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="color name:"> | ||||||
|  |         <InputCopyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="hex:"> | ||||||
|  |         <InputCopyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="rgb:"> | ||||||
|  |         <InputCopyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="hsl:"> | ||||||
|  |         <InputCopyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="hwb:"> | ||||||
|  |         <InputCopyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="lch:"> | ||||||
|  |         <InputCopyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="cmyk:"> | ||||||
|  |         <InputCopyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |     </n-form> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|   | |||||||
| @@ -1,92 +1,6 @@ | |||||||
| <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 { 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 +99,111 @@ const cronString = computed(() => { | |||||||
|   return ' '; |   return ' '; | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const cronValidation = useValidation({ | const cronValidationRules = [ | ||||||
|   source: cron, |  | ||||||
|   rules: [ |  | ||||||
|   { |   { | ||||||
|       validator: (value) => isCronValid(value), |     validator: (value: string) => isCronValid(value), | ||||||
|     message: 'This cron is invalid', |     message: 'This cron is invalid', | ||||||
|   }, |   }, | ||||||
|   ], | ]; | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <c-card> | ||||||
|  |     <div mx-auto max-w-sm> | ||||||
|  |       <c-input-text | ||||||
|  |         v-model:value="cron" | ||||||
|  |         size="large" | ||||||
|  |         placeholder="* * * * *" | ||||||
|  |         :validation-rules="cronValidationRules" | ||||||
|  |         mb-3 | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="cron-string"> | ||||||
|  |       {{ cronString }} | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <n-divider /> | ||||||
|  |  | ||||||
|  |     <div flex justify-center> | ||||||
|  |       <n-form :show-feedback="false" label-width="170" label-placement="left"> | ||||||
|  |         <n-form-item label="Verbose"> | ||||||
|  |           <n-switch v-model:value="cronstrueConfig.verbose" /> | ||||||
|  |         </n-form-item> | ||||||
|  |         <n-form-item label="Use 24 hour time format"> | ||||||
|  |           <n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" /> | ||||||
|  |         </n-form-item> | ||||||
|  |         <n-form-item label="Days start at 0"> | ||||||
|  |           <n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" /> | ||||||
|  |         </n-form-item> | ||||||
|  |       </n-form> | ||||||
|  |     </div> | ||||||
|  |   </c-card> | ||||||
|  |   <c-card> | ||||||
|  |     <pre> | ||||||
|  | ┌──────────── [optional] seconds (0 - 59) | ||||||
|  | | ┌────────── minute (0 - 59) | ||||||
|  | | | ┌──────── hour (0 - 23) | ||||||
|  | | | | ┌────── day of month (1 - 31) | ||||||
|  | | | | | ┌──── month (1 - 12) OR jan,feb,mar,apr ... | ||||||
|  | | | | | | ┌── day of week (0 - 6, sunday=0) OR sun,mon ... | ||||||
|  | | | | | | | | ||||||
|  | * * * * * * command</pre> | ||||||
|  |  | ||||||
|  |     <div v-if="styleStore.isSmallScreen"> | ||||||
|  |       <c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none> | ||||||
|  |         <div> | ||||||
|  |           Symbol: <strong>{{ symbol }}</strong> | ||||||
|  |         </div> | ||||||
|  |         <div> | ||||||
|  |           Meaning: <strong>{{ meaning }}</strong> | ||||||
|  |         </div> | ||||||
|  |         <div> | ||||||
|  |           Example: | ||||||
|  |           <strong><code>{{ example }}</code></strong> | ||||||
|  |         </div> | ||||||
|  |         <div> | ||||||
|  |           Equivalent: <strong>{{ equivalent }}</strong> | ||||||
|  |         </div> | ||||||
|  |       </c-card> | ||||||
|  |     </div> | ||||||
|  |     <n-table v-else size="small"> | ||||||
|  |       <thead> | ||||||
|  |         <tr> | ||||||
|  |           <th class="text-left" scope="col"> | ||||||
|  |             Symbol | ||||||
|  |           </th> | ||||||
|  |           <th class="text-left" scope="col"> | ||||||
|  |             Meaning | ||||||
|  |           </th> | ||||||
|  |           <th class="text-left" scope="col"> | ||||||
|  |             Example | ||||||
|  |           </th> | ||||||
|  |           <th class="text-left" scope="col"> | ||||||
|  |             Equivalent | ||||||
|  |           </th> | ||||||
|  |         </tr> | ||||||
|  |       </thead> | ||||||
|  |       <tbody> | ||||||
|  |         <tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol"> | ||||||
|  |           <td>{{ symbol }}</td> | ||||||
|  |           <td>{{ meaning }}</td> | ||||||
|  |           <td> | ||||||
|  |             <code>{{ example }}</code> | ||||||
|  |           </td> | ||||||
|  |           <td>{{ equivalent }}</td> | ||||||
|  |         </tr> | ||||||
|  |       </tbody> | ||||||
|  |     </n-table> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .cron { | ::v-deep(input) { | ||||||
|   text-align: center; |  | ||||||
|  |  | ||||||
|   margin: auto; |  | ||||||
|   max-width: 400px; |  | ||||||
|   display: block; |  | ||||||
|  |  | ||||||
|   .n-input { |  | ||||||
|   font-size: 30px; |   font-size: 30px; | ||||||
|   font-family: monospace; |   font-family: monospace; | ||||||
|   padding: 5px; |   padding: 5px; | ||||||
|   } |   text-align: center; | ||||||
| } | } | ||||||
|  |  | ||||||
| .cron-string { | .cron-string { | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { test, expect } from '@playwright/test'; | import { expect, test } from '@playwright/test'; | ||||||
|  |  | ||||||
| test.describe('Date time converter - json to yaml', () => { | test.describe('Date time converter - json to yaml', () => { | ||||||
|   test.beforeEach(async ({ page }) => { |   test.beforeEach(async ({ page }) => { | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
| import { describe, test, expect } from 'vitest'; | import { describe, expect, test } from 'vitest'; | ||||||
| import { | import { | ||||||
|   isISO8601DateTimeString, |   isISO8601DateTimeString, | ||||||
|   isISO9075DateString, |   isISO9075DateString, | ||||||
|  |   isMongoObjectId, | ||||||
|   isRFC3339DateString, |   isRFC3339DateString, | ||||||
|   isRFC7231DateString, |   isRFC7231DateString, | ||||||
|   isUnixTimestamp, |  | ||||||
|   isTimestamp, |   isTimestamp, | ||||||
|   isUTCDateString, |   isUTCDateString, | ||||||
|   isMongoObjectId, |   isUnixTimestamp, | ||||||
| } from './date-time-converter.models'; | } from './date-time-converter.models'; | ||||||
|  |  | ||||||
| describe('date-time-converter models', () => { | describe('date-time-converter models', () => { | ||||||
|   | |||||||
| @@ -11,13 +11,13 @@ export { | |||||||
|   isMongoObjectId, |   isMongoObjectId, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const ISO8601_REGEX = | const ISO8601_REGEX | ||||||
|   /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; |   = /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; | ||||||
| const ISO9075_REGEX = | const ISO9075_REGEX | ||||||
|   /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/; |   = /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/; | ||||||
|  |  | ||||||
| const RFC3339_REGEX = | const RFC3339_REGEX | ||||||
|   /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/; |   = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/; | ||||||
|  |  | ||||||
| const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/; | const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/; | ||||||
|  |  | ||||||
| @@ -40,7 +40,8 @@ function isUTCDateString(date?: string) { | |||||||
|  |  | ||||||
|   try { |   try { | ||||||
|     return new Date(date).toUTCString() === date; |     return new Date(date).toUTCString() === date; | ||||||
|   } catch (_ignored) { |   } | ||||||
|  |   catch (_ignored) { | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| export type ToDateMapper = (value: string) => Date; | export type ToDateMapper = (value: string) => Date; | ||||||
|  |  | ||||||
| export type DateFormat = { | export interface DateFormat { | ||||||
|   name: string; |   name: string | ||||||
|   fromDate: (date: Date) => string; |   fromDate: (date: Date) => string | ||||||
|   toDate: (value: string) => Date; |   toDate: (value: string) => Date | ||||||
|   formatMatcher: (dateString: string) => boolean; |   formatMatcher: (dateString: string) => boolean | ||||||
| }; | } | ||||||
|   | |||||||
| @@ -1,37 +1,3 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <n-form-item :show-label="false" v-bind="validation.attrs"> |  | ||||||
|       <n-input-group> |  | ||||||
|         <n-input |  | ||||||
|           v-model:value="inputDate" |  | ||||||
|           :on-input="onDateInputChanged" |  | ||||||
|           placeholder="Put you date string here..." |  | ||||||
|           clearable |  | ||||||
|           :input-props="{ 'data-test-id': 'date-time-converter-input' }" |  | ||||||
|         /> |  | ||||||
|  |  | ||||||
|         <n-select |  | ||||||
|           v-model:value="formatIndex" |  | ||||||
|           style="flex: 0 0 170px" |  | ||||||
|           :options="formats.map(({ name }, i) => ({ label: name, value: i }))" |  | ||||||
|           data-test-id="date-time-converter-format-select" |  | ||||||
|         /> |  | ||||||
|       </n-input-group> |  | ||||||
|     </n-form-item> |  | ||||||
|     <n-divider style="margin-top: 0" /> |  | ||||||
|     <div v-for="{ name, fromDate } in formats" :key="name" mt-1> |  | ||||||
|       <n-input-group> |  | ||||||
|         <n-input-group-label style="flex: 0 0 170px"> {{ name }}: </n-input-group-label> |  | ||||||
|         <input-copyable |  | ||||||
|           :value="formatDateUsingFormatter(fromDate, normalizedDate)" |  | ||||||
|           placeholder="Invalid date..." |  | ||||||
|           :input-props="{ 'data-test-id': name }" |  | ||||||
|         /> |  | ||||||
|       </n-input-group> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { | import { | ||||||
|   formatISO, |   formatISO, | ||||||
| @@ -41,41 +7,33 @@ import { | |||||||
|   fromUnixTime, |   fromUnixTime, | ||||||
|   getTime, |   getTime, | ||||||
|   getUnixTime, |   getUnixTime, | ||||||
|   parseISO, |  | ||||||
|   parseJSON, |  | ||||||
|   isDate, |   isDate, | ||||||
|   isValid, |   isValid, | ||||||
|  |   parseISO, | ||||||
|  |   parseJSON, | ||||||
| } from 'date-fns'; | } from 'date-fns'; | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; |  | ||||||
| import { useValidation } from '@/composable/validation'; |  | ||||||
| import type { DateFormat, ToDateMapper } from './date-time-converter.types'; | import type { DateFormat, ToDateMapper } from './date-time-converter.types'; | ||||||
| import { | import { | ||||||
|   isISO8601DateTimeString, |   isISO8601DateTimeString, | ||||||
|   isISO9075DateString, |   isISO9075DateString, | ||||||
|  |   isMongoObjectId, | ||||||
|   isRFC3339DateString, |   isRFC3339DateString, | ||||||
|   isRFC7231DateString, |   isRFC7231DateString, | ||||||
|   isTimestamp, |   isTimestamp, | ||||||
|   isUTCDateString, |   isUTCDateString, | ||||||
|   isUnixTimestamp, |   isUnixTimestamp, | ||||||
|   isMongoObjectId, |  | ||||||
| } from './date-time-converter.models'; | } from './date-time-converter.models'; | ||||||
|  | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
|  | import { useValidation } from '@/composable/validation'; | ||||||
|  |  | ||||||
| const inputDate = ref(''); | const inputDate = ref(''); | ||||||
|  |  | ||||||
| const toDate: ToDateMapper = (date) => new Date(date); | const toDate: ToDateMapper = date => new Date(date); | ||||||
|  |  | ||||||
| function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) { |  | ||||||
|   if (!date || !validation.isValid) { |  | ||||||
|     return ''; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return withDefaultOnError(() => formatter(date), ''); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const formats: DateFormat[] = [ | const formats: DateFormat[] = [ | ||||||
|   { |   { | ||||||
|     name: 'JS locale date string', |     name: 'JS locale date string', | ||||||
|     fromDate: (date) => date.toString(), |     fromDate: date => date.toString(), | ||||||
|     toDate, |     toDate, | ||||||
|     formatMatcher: () => false, |     formatMatcher: () => false, | ||||||
|   }, |   }, | ||||||
| @@ -83,49 +41,49 @@ const formats: DateFormat[] = [ | |||||||
|     name: 'ISO 8601', |     name: 'ISO 8601', | ||||||
|     fromDate: formatISO, |     fromDate: formatISO, | ||||||
|     toDate: parseISO, |     toDate: parseISO, | ||||||
|     formatMatcher: (date) => isISO8601DateTimeString(date), |     formatMatcher: date => isISO8601DateTimeString(date), | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'ISO 9075', |     name: 'ISO 9075', | ||||||
|     fromDate: formatISO9075, |     fromDate: formatISO9075, | ||||||
|     toDate: parseISO, |     toDate: parseISO, | ||||||
|     formatMatcher: (date) => isISO9075DateString(date), |     formatMatcher: date => isISO9075DateString(date), | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'RFC 3339', |     name: 'RFC 3339', | ||||||
|     fromDate: formatRFC3339, |     fromDate: formatRFC3339, | ||||||
|     toDate, |     toDate, | ||||||
|     formatMatcher: (date) => isRFC3339DateString(date), |     formatMatcher: date => isRFC3339DateString(date), | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'RFC 7231', |     name: 'RFC 7231', | ||||||
|     fromDate: formatRFC7231, |     fromDate: formatRFC7231, | ||||||
|     toDate, |     toDate, | ||||||
|     formatMatcher: (date) => isRFC7231DateString(date), |     formatMatcher: date => isRFC7231DateString(date), | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Unix timestamp', |     name: 'Unix timestamp', | ||||||
|     fromDate: (date) => String(getUnixTime(date)), |     fromDate: date => String(getUnixTime(date)), | ||||||
|     toDate: (sec) => fromUnixTime(+sec), |     toDate: sec => fromUnixTime(+sec), | ||||||
|     formatMatcher: (date) => isUnixTimestamp(date), |     formatMatcher: date => isUnixTimestamp(date), | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Timestamp', |     name: 'Timestamp', | ||||||
|     fromDate: (date) => String(getTime(date)), |     fromDate: date => String(getTime(date)), | ||||||
|     toDate: (ms) => parseJSON(+ms), |     toDate: ms => parseJSON(+ms), | ||||||
|     formatMatcher: (date) => isTimestamp(date), |     formatMatcher: date => isTimestamp(date), | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'UTC format', |     name: 'UTC format', | ||||||
|     fromDate: (date) => date.toUTCString(), |     fromDate: date => date.toUTCString(), | ||||||
|     toDate, |     toDate, | ||||||
|     formatMatcher: (date) => isUTCDateString(date), |     formatMatcher: date => isUTCDateString(date), | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Mongo ObjectID', |     name: 'Mongo ObjectID', | ||||||
|     fromDate: (date) => Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000', |     fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`, | ||||||
|     toDate: (objectId) => new Date(parseInt(objectId.substring(0, 8), 16) * 1000), |     toDate: objectId => new Date(Number.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> | ||||||
|  |     <div flex gap-2> | ||||||
|  |       <c-input-text | ||||||
|  |         v-model:value="inputDate" | ||||||
|  |         autofocus | ||||||
|  |         placeholder="Put your date string here..." | ||||||
|  |         clearable | ||||||
|  |         test-id="date-time-converter-input" | ||||||
|  |         :validation="validation" | ||||||
|  |         @update:value="onDateInputChanged" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <c-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" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <n-divider /> | ||||||
|  |  | ||||||
|  |     <input-copyable | ||||||
|  |       v-for="{ name, fromDate } in formats" | ||||||
|  |       :key="name" | ||||||
|  |       :label="name" | ||||||
|  |       label-width="150px" | ||||||
|  |       label-position="left" | ||||||
|  |       label-align="right" | ||||||
|  |       :value="formatDateUsingFormatter(fromDate, normalizedDate)" | ||||||
|  |       placeholder="Invalid date..." | ||||||
|  |       :test-id="name" | ||||||
|  |       readonly | ||||||
|  |       mt-2 | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|   | |||||||
| @@ -1,27 +1,5 @@ | |||||||
| <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'; |  | ||||||
|  |  | ||||||
| const { width, height } = useWindowSize(); | const { width, height } = useWindowSize(); | ||||||
|  |  | ||||||
| @@ -79,8 +57,33 @@ const sections = [ | |||||||
| ]; | ]; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <c-card v-for="{ name, information } in sections" :key="name" :title="name"> | ||||||
|  |     <n-grid cols="1 400:2" x-gap="12" y-gap="12"> | ||||||
|  |       <n-gi v-for="{ label, value: { value } } in information" :key="label" class="information"> | ||||||
|  |         <div class="label"> | ||||||
|  |           {{ label }} | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="value"> | ||||||
|  |           <n-ellipsis v-if="value"> | ||||||
|  |             {{ value }} | ||||||
|  |           </n-ellipsis> | ||||||
|  |           <div v-else class="undefined-value"> | ||||||
|  |             unknown | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </n-gi> | ||||||
|  |     </n-grid> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|  |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .information { | .information { | ||||||
|  |   padding: 14px 16px; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   background-color: #aaaaaa11; | ||||||
|  |  | ||||||
|   .label { |   .label { | ||||||
|     font-size: 14px; |     font-size: 14px; | ||||||
|     opacity: 0.8; |     opacity: 0.8; | ||||||
|   | |||||||
| @@ -1,84 +1,88 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <n-form-item label="Your docker run command:" :show-feedback="false"> |  | ||||||
|       <n-input |  | ||||||
|         v-model:value="dockerRun" |  | ||||||
|         style="font-family: monospace" |  | ||||||
|         type="textarea" |  | ||||||
|         placeholder="Your docker run command to convert..." |  | ||||||
|         rows="3" |  | ||||||
|       /> |  | ||||||
|     </n-form-item> |  | ||||||
|  |  | ||||||
|     <n-divider /> |  | ||||||
|  |  | ||||||
|     <textarea-copyable :value="dockerCompose" language="yaml" /> |  | ||||||
|     <br /> |  | ||||||
|     <br /> |  | ||||||
|     <n-space justify="center"> |  | ||||||
|       <n-button :disabled="dockerCompose === ''" secondary @click="download"> Download docker-compose.yml </n-button> |  | ||||||
|     </n-space> |  | ||||||
|  |  | ||||||
|     <div v-if="notComposable.length > 0"> |  | ||||||
|       <br /> |  | ||||||
|       <n-alert title="This options are not translatable to docker-compose" type="info"> |  | ||||||
|         <ul> |  | ||||||
|           <li v-for="(message, index) of notComposable" :key="index">{{ message }}</li> |  | ||||||
|         </ul> |  | ||||||
|       </n-alert> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div v-if="notImplemented.length > 0"> |  | ||||||
|       <br /> |  | ||||||
|       <n-alert |  | ||||||
|         title="This options are not yet implemented and therefore haven't been translated to docker-compose" |  | ||||||
|         type="warning" |  | ||||||
|       > |  | ||||||
|         <ul> |  | ||||||
|           <li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li> |  | ||||||
|         </ul> |  | ||||||
|       </n-alert> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div v-if="errors.length > 0"> |  | ||||||
|       <br /> |  | ||||||
|       <n-alert title="The following errors occured" type="error"> |  | ||||||
|         <ul> |  | ||||||
|           <li v-for="(message, index) of errors" :key="index">{{ message }}</li> |  | ||||||
|         </ul> |  | ||||||
|       </n-alert> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { computed, ref } from 'vue'; | import { MessageType, composerize } from 'composerize-ts'; | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
| import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||||||
| import { textToBase64 } from '@/utils/base64'; | import { textToBase64 } from '@/utils/base64'; | ||||||
| import TextareaCopyable from '@/components/TextareaCopyable.vue'; | import TextareaCopyable from '@/components/TextareaCopyable.vue'; | ||||||
|  |  | ||||||
| import { composerize, MessageType } from 'composerize-ts'; |  | ||||||
|  |  | ||||||
| const dockerRun = ref( | 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', |   '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(() => | const conversionResult = computed(() => | ||||||
|   withDefaultOnError(() => composerize(dockerRun.value), { yaml: '', messages: [] }), |   withDefaultOnError(() => composerize(dockerRun.value.trim()), { yaml: '', messages: [] }), | ||||||
| ); | ); | ||||||
| const dockerCompose = computed(() => conversionResult.value.yaml); | const dockerCompose = computed(() => conversionResult.value.yaml); | ||||||
| const notImplemented = computed(() => | const notImplemented = computed(() => | ||||||
|   conversionResult.value.messages.filter((msg) => msg.type === MessageType.notImplemented).map((msg) => msg.value), |   conversionResult.value.messages.filter(msg => msg.type === MessageType.notImplemented).map(msg => msg.value), | ||||||
| ); | ); | ||||||
| const notComposable = computed(() => | const notComposable = computed(() => | ||||||
|   conversionResult.value.messages.filter((msg) => msg.type === MessageType.notTranslatable).map((msg) => msg.value), |   conversionResult.value.messages.filter(msg => msg.type === MessageType.notTranslatable).map(msg => msg.value), | ||||||
| ); | ); | ||||||
| const errors = computed(() => | const errors = computed(() => | ||||||
|   conversionResult.value.messages |   conversionResult.value.messages | ||||||
|     .filter((msg) => msg.type === MessageType.errorDuringConversion) |     .filter(msg => msg.type === MessageType.errorDuringConversion) | ||||||
|     .map((msg) => msg.value), |     .map(msg => msg.value), | ||||||
| ); | ); | ||||||
| const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value)); | const dockerComposeBase64 = computed(() => `data:application/yaml;base64,${textToBase64(dockerCompose.value)}`); | ||||||
| const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' }); | const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-input-text | ||||||
|  |       v-model:value="dockerRun" | ||||||
|  |       label="Your docker run command:" | ||||||
|  |       style="font-family: monospace" | ||||||
|  |       multiline | ||||||
|  |       raw-text | ||||||
|  |       monospace | ||||||
|  |       placeholder="Your docker run command to convert..." | ||||||
|  |       rows="3" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <n-divider /> | ||||||
|  |  | ||||||
|  |     <TextareaCopyable :value="dockerCompose" language="yaml" /> | ||||||
|  |  | ||||||
|  |     <div mt-5 flex justify-center> | ||||||
|  |       <c-button :disabled="dockerCompose === ''" secondary @click="download"> | ||||||
|  |         Download docker-compose.yml | ||||||
|  |       </c-button> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div v-if="notComposable.length > 0"> | ||||||
|  |       <n-alert title="This options are not translatable to docker-compose" type="info" mt-5> | ||||||
|  |         <ul> | ||||||
|  |           <li v-for="(message, index) of notComposable" :key="index"> | ||||||
|  |             {{ message }} | ||||||
|  |           </li> | ||||||
|  |         </ul> | ||||||
|  |       </n-alert> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div v-if="notImplemented.length > 0"> | ||||||
|  |       <n-alert | ||||||
|  |         title="This options are not yet implemented and therefore haven't been translated to docker-compose" | ||||||
|  |         type="warning" | ||||||
|  |         mt-5 | ||||||
|  |       > | ||||||
|  |         <ul> | ||||||
|  |           <li v-for="(message, index) of notImplemented" :key="index"> | ||||||
|  |             {{ message }} | ||||||
|  |           </li> | ||||||
|  |         </ul> | ||||||
|  |       </n-alert> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div v-if="errors.length > 0"> | ||||||
|  |       <n-alert title="The following errors occured" type="error" mt-5> | ||||||
|  |         <ul> | ||||||
|  |           <li v-for="(message, index) of errors" :key="index"> | ||||||
|  |             {{ message }} | ||||||
|  |           </li> | ||||||
|  |         </ul> | ||||||
|  |       </n-alert> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|   | |||||||
							
								
								
									
										42
									
								
								src/tools/emoji-picker/emoji-card.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/tools/emoji-picker/emoji-card.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import type { EmojiInfo } from './emoji.types'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  |  | ||||||
|  | const props = (defineProps<{ emojiInfo: EmojiInfo }>()); | ||||||
|  | const { emojiInfo } = toRefs(props); | ||||||
|  |  | ||||||
|  | const { copy } = useCopy(); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <c-card flex items-center gap-3 important:py-8px important:pl-10px important:pr-5px> | ||||||
|  |     <div cursor-pointer text-30px @click="copy(emojiInfo.emoji, { notificationMessage: `Emoji ${emojiInfo.emoji} copied to the clipboard` })"> | ||||||
|  |       {{ emojiInfo.emoji }} | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div min-w-0 flex-1> | ||||||
|  |       <div truncate font-bold> | ||||||
|  |         {{ emojiInfo.title }} | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <!-- <div> | ||||||
|  |         <c-link> | ||||||
|  |           {{ emojiInfo.codePoints }} | ||||||
|  |         </c-link> | ||||||
|  |       </div> | ||||||
|  |       <div /> | ||||||
|  |       <div rounded op-70> | ||||||
|  |         Unicode:  <span border="1px solid current op-30" b-rd-xl px-12px py-4px>{{ emojiInfo.unicode }}</span> | ||||||
|  |       </div> --> | ||||||
|  |  | ||||||
|  |       <div flex gap-2 text-xs font-mono op-70> | ||||||
|  |         <span cursor-pointer transition hover:text-primary @click="copy(emojiInfo.codePoints, { notificationMessage: `Code points '${emojiInfo.codePoints}' copied to the clipboard` })"> | ||||||
|  |           {{ emojiInfo.codePoints }} | ||||||
|  |         </span> | ||||||
|  |         <span cursor-pointer truncate transition hover:text-primary @click="copy(emojiInfo.unicode, { notificationMessage: `Unicode '${emojiInfo.unicode}' copied to the clipboard` })"> | ||||||
|  |           {{ emojiInfo.unicode }} | ||||||
|  |         </span> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
							
								
								
									
										12
									
								
								src/tools/emoji-picker/emoji-grid.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/emoji-picker/emoji-grid.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import type { EmojiInfo } from './emoji.types'; | ||||||
|  |  | ||||||
|  | const props = withDefaults(defineProps<{ emojiInfos?: EmojiInfo[] }>(), { emojiInfos: () => [] }); | ||||||
|  | const { emojiInfos } = toRefs(props); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <div grid grid-cols-1 gap-2 lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 xl:grid-cols-6> | ||||||
|  |     <emoji-card v-for="emojiInfo in emojiInfos" :key="emojiInfo.name" :emoji-info="emojiInfo" flex items-center gap-3 /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
							
								
								
									
										85
									
								
								src/tools/emoji-picker/emoji-picker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/tools/emoji-picker/emoji-picker.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import emojiUnicodeData from 'unicode-emoji-json'; | ||||||
|  | import emojiKeywords from 'emojilib'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import type { EmojiInfo } from './emoji.types'; | ||||||
|  | import { useFuzzySearch } from '@/composable/fuzzySearch'; | ||||||
|  |  | ||||||
|  | const escapeUnicode = ({ emoji }: { emoji: string }) => emoji.split('').map(unit => `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`).join(''); | ||||||
|  | const getEmojiCodePoints = ({ emoji }: { emoji: string }) => emoji.codePointAt(0) ? `0x${emoji.codePointAt(0)?.toString(16)}` : undefined; | ||||||
|  |  | ||||||
|  | const emojis = _.map(emojiUnicodeData, (emojiInfo, emoji) => ({ | ||||||
|  |   ...emojiInfo, | ||||||
|  |   emoji, | ||||||
|  |   title: _.capitalize(emojiInfo.name), | ||||||
|  |   keywords: emojiKeywords[emoji as keyof typeof emojiKeywords], | ||||||
|  |   codePoints: getEmojiCodePoints({ emoji }), | ||||||
|  |   unicode: escapeUnicode({ emoji }), | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | const emojisGroups: { emojiInfos: EmojiInfo[]; group: string }[] = _ | ||||||
|  |   .chain(emojis) | ||||||
|  |   .groupBy('group') | ||||||
|  |   .map((emojiInfos, group) => ({ group, emojiInfos })) | ||||||
|  |   .value(); | ||||||
|  |  | ||||||
|  | const searchQuery = ref(''); | ||||||
|  |  | ||||||
|  | const { searchResult } = useFuzzySearch({ | ||||||
|  |   search: searchQuery, | ||||||
|  |   data: emojis, | ||||||
|  |   options: { | ||||||
|  |     keys: ['group', { name: 'name', weight: 3 }, 'keywords', 'unicode', 'codePoints', 'emoji'], | ||||||
|  |     threshold: 0.3, | ||||||
|  |     useExtendedSearch: true, | ||||||
|  |     isCaseSensitive: false, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |   <div mx-auto max-w-2400px important:flex-1> | ||||||
|  |     <div flex items-center gap-3> | ||||||
|  |       <c-input-text | ||||||
|  |         v-model:value="searchQuery" | ||||||
|  |         placeholder="Search emojis (e.g. 'smile')..." | ||||||
|  |         mx-auto max-w-600px | ||||||
|  |       > | ||||||
|  |         <template #prefix> | ||||||
|  |           <icon-mdi-search mr-6px color-black op-70 dark:color-white /> | ||||||
|  |         </template> | ||||||
|  |       </c-input-text> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div v-if="searchQuery.trim().length > 0"> | ||||||
|  |       <div | ||||||
|  |         v-if="searchResult.length === 0" | ||||||
|  |         mt-4 | ||||||
|  |         text-20px | ||||||
|  |         font-bold | ||||||
|  |       > | ||||||
|  |         No results | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div v-else> | ||||||
|  |         <div mt-4 text-20px font-bold> | ||||||
|  |           Search result | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <emoji-grid :emoji-infos="searchResult" /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div | ||||||
|  |       v-for="{ group, emojiInfos } in emojisGroups" | ||||||
|  |       v-else | ||||||
|  |       :key="group" | ||||||
|  |     > | ||||||
|  |       <div mt-4 text-20px font-bold> | ||||||
|  |         {{ group }} | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <emoji-grid :emoji-infos="emojiInfos" /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
							
								
								
									
										8
									
								
								src/tools/emoji-picker/emoji.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/tools/emoji-picker/emoji.types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | import type emojiUnicodeData from 'unicode-emoji-json'; | ||||||
|  |  | ||||||
|  | export type EmojiInfo = { | ||||||
|  |   title: string | ||||||
|  |   emoji: string | ||||||
|  |   codePoints: string | undefined | ||||||
|  |   unicode: string | ||||||
|  | } & typeof emojiUnicodeData[string]; | ||||||
							
								
								
									
										12
									
								
								src/tools/emoji-picker/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/emoji-picker/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | import { MoodSmile } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  |  | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'Emoji picker', | ||||||
|  |   path: '/emoji-picker', | ||||||
|  |   description: 'Copy and paste emojis easily and get the unicode and code points value of each emoji.', | ||||||
|  |   keywords: ['emoji', 'picker', 'unicode', 'copy', 'paste'], | ||||||
|  |   component: () => import('./emoji-picker.vue'), | ||||||
|  |   icon: MoodSmile, | ||||||
|  |   createdAt: new Date('2023-08-07'), | ||||||
|  | }); | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user