Merge branch 'CorentinTh:main' into tools/chmod-calculator
This commit is contained in:
		
						commit
						fe45e0e923
					
				| @ -10,5 +10,12 @@ module.exports = { | |||||||
|     '@typescript-eslint/semi': ['error', 'always'], |     '@typescript-eslint/semi': ['error', 'always'], | ||||||
|     '@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }], |     '@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }], | ||||||
|     'vue/no-empty-component-block': ['error'], |     'vue/no-empty-component-block': ['error'], | ||||||
|  |     'no-restricted-imports': ['error', { | ||||||
|  |       paths: [{ | ||||||
|  |         name: '@vueuse/core', | ||||||
|  |         importNames: ['useClipboard'], | ||||||
|  |         message: 'Please use local useCopy from src/composable/copy.ts instead of useClipboard.', | ||||||
|  |       }], | ||||||
|  |     }], | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @ -11,7 +11,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
| 
 | 
 | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||||
|       - run: corepack enable |       - run: corepack enable | ||||||
|       - uses: actions/setup-node@v3 |       - uses: actions/setup-node@v3 | ||||||
|         with: |         with: | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @ -37,7 +37,7 @@ jobs: | |||||||
| 
 | 
 | ||||||
|     steps: |     steps: | ||||||
|     - name: Checkout repository |     - name: Checkout repository | ||||||
|       uses: actions/checkout@v3 |       uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||||
| 
 | 
 | ||||||
|     # Initializes the CodeQL tools for scanning. |     # Initializes the CodeQL tools for scanning. | ||||||
|     - name: Initialize CodeQL |     - name: Initialize CodeQL | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								.github/workflows/docker-nightly-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.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@v3 |       - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||||
|       - name: print latest_commit |       - name: print latest_commit | ||||||
|         run: echo ${{ github.sha }} |         run: echo ${{ github.sha }} | ||||||
| 
 | 
 | ||||||
| @ -28,7 +28,7 @@ jobs: | |||||||
|     if: ${{ needs.check_date.outputs.should_run != 'false' }} |     if: ${{ needs.check_date.outputs.should_run != 'false' }} | ||||||
| 
 | 
 | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||||
|       - run: corepack enable |       - run: corepack enable | ||||||
|       - uses: actions/setup-node@v3 |       - uses: actions/setup-node@v3 | ||||||
|         with: |         with: | ||||||
| @ -54,7 +54,7 @@ jobs: | |||||||
| 
 | 
 | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout |       - name: Checkout | ||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||||
| 
 | 
 | ||||||
|       - name: Login to GitHub Container Registry |       - name: Login to GitHub Container Registry | ||||||
|         uses: docker/login-action@v2 |         uses: docker/login-action@v2 | ||||||
|  | |||||||
							
								
								
									
										15
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,18 +1,18 @@ | |||||||
| name: E2E tests | name: E2E tests | ||||||
| on: [deployment_status] | on: | ||||||
| 
 |   pull_request: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
| jobs: | jobs: | ||||||
|   test: |   test: | ||||||
|     if: github.event.deployment_status.state == 'success' |  | ||||||
|     timeout-minutes: 60 |     timeout-minutes: 60 | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     env: |  | ||||||
|       BASE_URL: ${{ github.event.deployment_status.target_url }} |  | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         shard: [1/3, 2/3, 3/3] |         shard: [1/3, 2/3, 3/3] | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||||
| 
 | 
 | ||||||
|       - run: corepack enable |       - run: corepack enable | ||||||
| 
 | 
 | ||||||
| @ -28,6 +28,9 @@ jobs: | |||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: pnpm install |         run: pnpm install | ||||||
| 
 | 
 | ||||||
|  |       - name: Build app | ||||||
|  |         run: pnpm build | ||||||
|  | 
 | ||||||
|       - name: Restore Playwright browsers from cache |       - name: Restore Playwright browsers from cache | ||||||
|         uses: actions/cache@v3 |         uses: actions/cache@v3 | ||||||
|         with: |         with: | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								.github/workflows/releases.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/releases.yml
									
									
									
									
										vendored
									
									
								
							| @ -13,7 +13,7 @@ jobs: | |||||||
|         run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV |         run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV | ||||||
| 
 | 
 | ||||||
|       - name: Checkout |       - name: Checkout | ||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||||
| 
 | 
 | ||||||
|       - name: Login to GitHub Container Registry |       - name: Login to GitHub Container Registry | ||||||
|         uses: docker/login-action@v2 |         uses: docker/login-action@v2 | ||||||
| @ -55,7 +55,7 @@ jobs: | |||||||
|         run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV |         run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV | ||||||
| 
 | 
 | ||||||
|       - name: Checkout |       - name: Checkout | ||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||||
| 
 | 
 | ||||||
|       - run: corepack enable |       - run: corepack enable | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										118
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										118
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -2,6 +2,124 @@ | |||||||
| 
 | 
 | ||||||
| All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. | ||||||
| 
 | 
 | ||||||
|  | ## Version 2023.08.21-6f93cba | ||||||
|  | 
 | ||||||
|  | ### Features | ||||||
|  | - **copy**: support legacy copy to clipboard for older browser (#581) (6f93cba) | ||||||
|  | - **new tool**: string obfuscator (#575) (c58d6e3) | ||||||
|  | 
 | ||||||
|  | ### Bug fixes | ||||||
|  | - **deps**: update dependency sql-formatter to v12 (#520) (2bcb77a) | ||||||
|  | 
 | ||||||
|  | ### 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 | ||||||
|  | - **Case Converter**: Add lowercase and uppercase (#534) (7b6232a) | ||||||
|  | - **new tool**: emoji picker (#551) (93f7cf0) | ||||||
|  | - **ui**: added c-select in the ui lib (#550) (dfa1ba8) | ||||||
|  | - **new-tool**: password strength analyzer (#502) (a9c7b89) | ||||||
|  | - **new-tool**: yaml to toml (e29b258) | ||||||
|  | - **new-tool**: json to toml (ea50a3f) | ||||||
|  | - **new-tool**: toml to yaml (746e5bd) | ||||||
|  | - **new-tool**: toml to json (c7d4f11) | ||||||
|  | - **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) | ||||||
|  | - **new-tool**: json to csv converter (69f0bd0) | ||||||
|  | - **new tool**: xml formatter (#457) (a6bbeae) | ||||||
|  | - **chmod-calculator**: added symbolic representation (#455) (f771e7a) | ||||||
|  | - **enhancement**: use system dark mode (#458) (cf7b1f0) | ||||||
|  | - **phone-parser**: searchable country code select (d2956b6) | ||||||
|  | - **new tool**: camera screenshot and recorder (34d8e5c) | ||||||
|  | - **base64-string-converter**: switch to encode and decode url safe base64 strings (#392) (0b20f1c) | ||||||
|  | 
 | ||||||
|  | ### Bug fixes | ||||||
|  | - **deps**: update dependency uuid to v9 (#566) (5e12991) | ||||||
|  | - **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) | ||||||
|  | - **c-lib**: hide component library shortcut link in non-dev (#557) (56d74d0) | ||||||
|  | - **emoji picker**: fix copy button (#556) (e5d0ba7) | ||||||
|  | - **deps**: update dependency @vueuse/head to v1 (#515) (d12dd40) | ||||||
|  | - **deps**: update dependency country-code-lookup to ^0.1.0 (#493) (8c72e69) | ||||||
|  | - **deps**: update dependency @vueuse/head to ^0.9.0 (#492) (cec9dea) | ||||||
|  | - **i18n**: fallback for demo i18n (12d9e5d) | ||||||
|  | - **typos**: fixed more typos & uppercase JSON (#475) (9526ed8) | ||||||
|  | - **about**: typos and wording (#474) (7068610) | ||||||
|  | - **mime-types**: typos (#470) (c4cec9e) | ||||||
|  | - **sonar**: took down minor sonar warning (4cbd7ac) | ||||||
|  | - **readme**: typo (105b21b) | ||||||
|  | - **ipv4-range-expander**: calculate correct for ip addresses where the first octet is lower than 128 (#405) (8c92d56) | ||||||
|  | - **ipv4-converter**: removed readonly on input (7aed9c5) | ||||||
|  | 
 | ||||||
|  | ### Refactoring | ||||||
|  | - **navbar**: consistent spacing in navbar buttons (#507) (30f88fc) | ||||||
|  | - **ui**: remove n-text (#506) (72c98a3) | ||||||
|  | - **ui**: replaced some n-input to c-input (#505) (05ea545) | ||||||
|  | - **json-viewer**: input monospace font (#485) (9125dcf) | ||||||
|  | - **search**: command palette design (#463) (bcb98b3) | ||||||
|  | - **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) | ||||||
|  | - **ui**: removed all n-space (4d2b037) | ||||||
|  | - **ui**: replaced some n-input with c-input-text (f7fc779) | ||||||
|  | 
 | ||||||
|  | ### Chores | ||||||
|  | - **deps**: update dependency vitest to ^0.34.0 (#562) (9bd4ad4) | ||||||
|  | - **deps**: update dependency node to v18.17.1 (#560) (65a9474) | ||||||
|  | - **deps**: update dependency unocss to ^0.55.0 (#561) (85cc7a8) | ||||||
|  | - **deps**: update dependency @unocss/eslint-config to ^0.55.0 (#553) (4268e25) | ||||||
|  | - **deps**: update dependency @intlify/unplugin-vue-i18n to ^0.12.0 (#526) (d1c8880) | ||||||
|  | - **deps**: update docker/login-action action to v2 (#512) (99bc84c) | ||||||
|  | - **deps**: update dependency jsdom to v22 (#499) (cd5a503) | ||||||
|  | - **deps**: update dependency @vitejs/plugin-vue-jsx to v3 (#497) (1a60236) | ||||||
|  | - **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) | ||||||
|  | - **ci**: e2e against vercel deployement (#518) (2e28c50) | ||||||
|  | - **e2e**: execute e2e against built app (#511) (cf382b5) | ||||||
|  | - **deps**: update github/codeql-action action to v2 (#513) (0152583) | ||||||
|  | - **deps**: update node.js to v18 (#514) (38cb61d) | ||||||
|  | - **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) | ||||||
|  | - **deps**: update dependency typescript to ~4.9.0 (#481) (f440507) | ||||||
|  | - **deps**: update dependency vue-tsc to ^0.40.0 (#490) (b0d9a3e) | ||||||
|  | - **deps**: updated unplugin-auto-import (#504) (5c3bebf) | ||||||
|  | - **deps**: removed start-server-and-test dependency (8df7cd0) | ||||||
|  | - **deps**: update dependency c8 to v8 (#498) (6bda2ca) | ||||||
|  | - **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) | ||||||
|  | - **deps**: update actions/checkout action to v3 (#494) (3f7d469) | ||||||
|  | - **deps**: update dependency unplugin-vue-components to ^0.25.0 (#484) (5f21908) | ||||||
|  | - **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) | ||||||
|  | - **deps**: added renovate.json (#477) (363c2e4) | ||||||
|  | - **i18n**: tool scoped locales (#471) (1b038c7) | ||||||
|  | - **wysiwyg-editor**: update tiptap dependencies (732da08) | ||||||
|  | - **i18n**: setup i18n plugin config (ebfb872) | ||||||
|  | - **config**: netlify deployment support (#443) (93799af) | ||||||
|  | - **ci**: shard e2e tests (962a6d6) | ||||||
|  | - **lint**: switched to a better lint config (33c9b66) | ||||||
|  | 
 | ||||||
|  | ### Refacor | ||||||
|  | - **transformers**: use monospace font for JSON and SQL text areas (#476) (ba4876d) | ||||||
|  | 
 | ||||||
|  | ### Documentation | ||||||
|  | - **ide**: updated vscode extensions settings (#472) (847323c) | ||||||
|  | 
 | ||||||
|  | ### Chors | ||||||
|  | - **deps**: updated vueuse dependency version (8515c24) | ||||||
|  | 
 | ||||||
| ## Version 2023.05.14-77f2efc | ## Version 2023.05.14-77f2efc | ||||||
| 
 | 
 | ||||||
| ### Features | ### Features | ||||||
|  | |||||||
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								README.md
									
									
									
									
									
								
							| @ -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) | ||||||
| 
 | 
 | ||||||
| @ -34,23 +35,21 @@ 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/) with the following extensions: | [VSCode](https://code.visualstudio.com/) with the following extensions: | ||||||
| - [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur)  | 
 | ||||||
|  | - [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). | - [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) | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) | ||||||
| - [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) | - [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) | ||||||
| 
 | 
 | ||||||
| with the following settings: | with the following settings: | ||||||
| 
 | 
 | ||||||
| ```json5 | ```json | ||||||
| { | { | ||||||
|   "editor.formatOnSave": false, |   "editor.formatOnSave": false, | ||||||
|   "editor.codeActionsOnSave": { |   "editor.codeActionsOnSave": { | ||||||
|     "source.fixAll.eslint": true |     "source.fixAll.eslint": true | ||||||
|   }, |   }, | ||||||
|   "i18n-ally.localesPaths": [ |   "i18n-ally.localesPaths": ["locales", "src/tools/*/locales"], | ||||||
|     "locales", |  | ||||||
|     "src/tools/*/locales" |  | ||||||
|   ], |  | ||||||
|   "i18n-ally.keystyle": "nested" |   "i18n-ally.keystyle": "nested" | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
| @ -106,12 +105,20 @@ 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 imported 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. | ||||||
| 
 | 
 | ||||||
|  | ## Contributors | ||||||
|  | 
 | ||||||
|  | Big thanks to all the people who have already contributed! | ||||||
|  | 
 | ||||||
|  | [](https://github.com/corentinth/it-tools/graphs/contributors) | ||||||
|  | 
 | ||||||
| ## Credits | ## Credits | ||||||
| 
 | 
 | ||||||
| Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr). | Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr). | ||||||
| 
 | 
 | ||||||
| This project is continuously deployed using [vercel.com](https://vercel.com). | This project is continuously deployed using [vercel.com](https://vercel.com). | ||||||
| 
 | 
 | ||||||
|  | Contributor graph is generated using [contrib.rocks](https://contrib.rocks/preview?repo=corentinth/it-tools). | ||||||
|  | 
 | ||||||
| <a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> | <a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> | ||||||
| <a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=345793&theme=light&period=daily" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> | <a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=345793&theme=light&period=daily" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										24
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -25,12 +25,17 @@ declare module '@vue/runtime-core' { | |||||||
|     CaseConverter: typeof import('./src/tools/case-converter/case-converter.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: typeof import('./src/ui/c-button/c-button.vue')['default'] | ||||||
|     'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default'] |     'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default'] | ||||||
|  |     CButtonsSelect: typeof import('./src/ui/c-buttons-select/c-buttons-select.vue')['default'] | ||||||
|  |     'CButtonsSelect.demo': typeof import('./src/ui/c-buttons-select/c-buttons-select.demo.vue')['default'] | ||||||
|     CCard: typeof import('./src/ui/c-card/c-card.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'] |     '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'] |     ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default'] | ||||||
|     Chronometer: typeof import('./src/tools/chronometer/chronometer.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: 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'] |     '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'] |     CLabel: typeof import('./src/ui/c-label/c-label.vue')['default'] | ||||||
|     CLink: typeof import('./src/ui/c-link/c-link.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'] |     'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] | ||||||
| @ -44,8 +49,11 @@ declare module '@vue/runtime-core' { | |||||||
|     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.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: typeof import('./src/ui/c-select/c-select.vue')['default'] | ||||||
|     'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.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'] |     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] | ||||||
|     'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default'] |  | ||||||
|     'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default'] |     'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default'] | ||||||
|     DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] |     DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] | ||||||
|     DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default'] |     DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default'] | ||||||
| @ -68,12 +76,12 @@ declare module '@vue/runtime-core' { | |||||||
|     HtmlEntities: typeof import('./src/tools/html-entities/html-entities.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'] |     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'] |     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: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'] |     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] | ||||||
|     IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] |     IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] | ||||||
|     IconMdiCamera: typeof import('~icons/mdi/camera')['default'] |     IconMdiCamera: typeof import('~icons/mdi/camera')['default'] | ||||||
|     IconMdiCameraOutline: typeof import('~icons/mdi/camera-outline')['default'] |  | ||||||
|     IconMdiCameraVideoOff: typeof import('~icons/mdi/camera-video-off')['default'] |  | ||||||
|     IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] |     IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] | ||||||
|     IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] |     IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] | ||||||
|     IconMdiClose: typeof import('~icons/mdi/close')['default'] |     IconMdiClose: typeof import('~icons/mdi/close')['default'] | ||||||
| @ -82,14 +90,11 @@ declare module '@vue/runtime-core' { | |||||||
|     IconMdiDownload: typeof import('~icons/mdi/download')['default'] |     IconMdiDownload: typeof import('~icons/mdi/download')['default'] | ||||||
|     IconMdiEye: typeof import('~icons/mdi/eye')['default'] |     IconMdiEye: typeof import('~icons/mdi/eye')['default'] | ||||||
|     IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] |     IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] | ||||||
|     IconMdiMagnify: typeof import('~icons/mdi/magnify')['default'] |  | ||||||
|     IconMdiPause: typeof import('~icons/mdi/pause')['default'] |     IconMdiPause: typeof import('~icons/mdi/pause')['default'] | ||||||
|     IconMdiPlay: typeof import('~icons/mdi/play')['default'] |     IconMdiPlay: typeof import('~icons/mdi/play')['default'] | ||||||
|     IconMdiRecord: typeof import('~icons/mdi/record')['default'] |     IconMdiRecord: typeof import('~icons/mdi/record')['default'] | ||||||
|     IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] |     IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] | ||||||
|     IconMdiSearch: typeof import('~icons/mdi/search')['default'] |     IconMdiSearch: typeof import('~icons/mdi/search')['default'] | ||||||
|     IconMdiSearchRound: typeof import('~icons/mdi/search-round')['default'] |  | ||||||
|     IconMdiTea: typeof import('~icons/mdi/tea')['default'] |  | ||||||
|     IconMdiVideo: typeof import('~icons/mdi/video')['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'] |     IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] | ||||||
| @ -135,7 +140,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'] | ||||||
| @ -146,13 +150,11 @@ declare module '@vue/runtime-core' { | |||||||
|     NPageHeader: typeof import('naive-ui')['NPageHeader'] |     NPageHeader: typeof import('naive-ui')['NPageHeader'] | ||||||
|     NProgress: typeof import('naive-ui')['NProgress'] |     NProgress: typeof import('naive-ui')['NProgress'] | ||||||
|     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'] | ||||||
|     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'] | ||||||
| @ -170,9 +172,11 @@ declare module '@vue/runtime-core' { | |||||||
|     SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default'] |     SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default'] | ||||||
|     SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] |     SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] | ||||||
|     SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.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'] |     SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default'] | ||||||
|     TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.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'] |     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'] |     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'] |     TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default'] | ||||||
| @ -181,11 +185,13 @@ declare module '@vue/runtime-core' { | |||||||
|     TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.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'] |     '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'] | ||||||
|  |     UlidGenerator: typeof import('./src/tools/ulid-generator/ulid-generator.vue')['default'] | ||||||
|     UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default'] |     UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default'] | ||||||
|     UrlParser: typeof import('./src/tools/url-parser/url-parser.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'] |     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'] |     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'] |     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'] |     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'] |     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'] |     YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default'] | ||||||
|  | |||||||
							
								
								
									
										44
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								package.json
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "it-tools", |   "name": "it-tools", | ||||||
|   "version": "2023.5.14-77f2efc", |   "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,11 +21,12 @@ | |||||||
|   }, |   }, | ||||||
|   "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", | ||||||
| @ -36,12 +37,12 @@ | |||||||
|     "@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.3", |     "@tiptap/pm": "^2.1.6", | ||||||
|     "@tiptap/starter-kit": "^2.0.3", |     "@tiptap/starter-kit": "^2.1.6", | ||||||
|     "@tiptap/vue-3": "^2.0.3", |     "@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": "^10.1.2", |     "@vueuse/core": "^10.3.0", | ||||||
|     "@vueuse/head": "^1.0.0", |     "@vueuse/head": "^1.0.0", | ||||||
|     "@vueuse/router": "^10.0.0", |     "@vueuse/router": "^10.0.0", | ||||||
|     "bcryptjs": "^2.4.3", |     "bcryptjs": "^2.4.3", | ||||||
| @ -58,12 +59,14 @@ | |||||||
|     "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", |     "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", |     "libphonenumber-js": "^1.10.28", | ||||||
|     "lodash": "^4.17.21", |     "lodash": "^4.17.21", | ||||||
|     "mathjs": "^11.0.0", |     "mathjs": "^11.9.1", | ||||||
|     "mime-types": "^2.1.35", |     "mime-types": "^2.1.35", | ||||||
|  |     "monaco-editor": "^0.43.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", | ||||||
| @ -72,9 +75,9 @@ | |||||||
|     "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", | ||||||
|  |     "ulid": "^2.3.0", | ||||||
|     "unicode-emoji-json": "^0.4.0", |     "unicode-emoji-json": "^0.4.0", | ||||||
|     "unplugin-auto-import": "^0.16.4", |     "unplugin-auto-import": "^0.16.4", | ||||||
|     "uuid": "^9.0.0", |     "uuid": "^9.0.0", | ||||||
| @ -86,44 +89,43 @@ | |||||||
|     "yaml": "^2.2.1" |     "yaml": "^2.2.1" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@antfu/eslint-config": "^0.39.3", |     "@antfu/eslint-config": "^0.41.0", | ||||||
|     "@iconify-json/mdi": "^1.1.50", |     "@iconify-json/mdi": "^1.1.50", | ||||||
|     "@intlify/unplugin-vue-i18n": "^0.12.0", |     "@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": "^21.0.0", |     "@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": "^18.0.0", |     "@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": "^9.0.0", |     "@types/uuid": "^9.0.0", | ||||||
|     "@typescript-eslint/parser": "^5.58.0", |  | ||||||
|     "@unocss/eslint-config": "^0.55.0", |     "@unocss/eslint-config": "^0.55.0", | ||||||
|     "@vitejs/plugin-vue": "^4.0.0", |     "@vitejs/plugin-vue": "^4.3.2", | ||||||
|     "@vitejs/plugin-vue-jsx": "^3.0.0", |     "@vitejs/plugin-vue-jsx": "^3.0.2", | ||||||
|     "@vue/compiler-sfc": "^3.2.47", |     "@vue/compiler-sfc": "^3.2.47", | ||||||
|     "@vue/runtime-dom": "^3.3.4", |     "@vue/runtime-dom": "^3.3.4", | ||||||
|     "@vue/test-utils": "^2.3.2", |     "@vue/test-utils": "^2.3.2", | ||||||
|     "@vue/tsconfig": "^0.1.3", |     "@vue/tsconfig": "^0.4.0", | ||||||
|     "c8": "^8.0.0", |     "c8": "^8.0.0", | ||||||
|     "consola": "^3.0.2", |     "consola": "^3.0.2", | ||||||
|     "eslint": "^8.38.0", |     "eslint": "^8.47.0", | ||||||
|     "jsdom": "^22.0.0", |     "jsdom": "^22.0.0", | ||||||
|     "less": "^4.1.3", |     "less": "^4.1.3", | ||||||
|     "prettier": "^2.8.7", |     "prettier": "^3.0.0", | ||||||
|     "typescript": "~4.9.0", |     "typescript": "~5.2.0", | ||||||
|     "unocss": "^0.55.0", |     "unocss": "^0.55.0", | ||||||
|     "unocss-preset-scrollbar": "^0.2.1", |     "unocss-preset-scrollbar": "^0.2.1", | ||||||
|     "unplugin-icons": "^0.16.1", |     "unplugin-icons": "^0.17.0", | ||||||
|     "unplugin-vue-components": "^0.25.0", |     "unplugin-vue-components": "^0.25.0", | ||||||
|     "vite": "^4.0.0", |     "vite": "^4.4.9", | ||||||
|     "vite-plugin-pwa": "^0.16.0", |     "vite-plugin-pwa": "^0.16.0", | ||||||
|     "vite-plugin-vue-markdown": "^0.23.5", |     "vite-plugin-vue-markdown": "^0.23.5", | ||||||
|     "vite-svg-loader": "^4.0.0", |     "vite-svg-loader": "^4.0.0", | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import { defineConfig, devices } from '@playwright/test'; | |||||||
| 
 | 
 | ||||||
| const isCI = !!process.env.CI; | const isCI = !!process.env.CI; | ||||||
| const baseUrl = process.env.BASE_URL || 'http://localhost:5050'; | const baseUrl = process.env.BASE_URL || 'http://localhost:5050'; | ||||||
|  | const useWebServer = process.env.NO_WEB_SERVER !== 'true'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * See https://playwright.dev/docs/test-configuration.
 |  * See https://playwright.dev/docs/test-configuration.
 | ||||||
| @ -52,13 +53,13 @@ export default defineConfig({ | |||||||
| 
 | 
 | ||||||
|   /* Run your local dev server before starting the tests */ |   /* Run your local dev server before starting the tests */ | ||||||
| 
 | 
 | ||||||
|   ...(isCI |   ...(useWebServer | ||||||
|     ? {} |     && { | ||||||
|     : { |       webServer: { | ||||||
|         webServer: { |         command: 'npm run preview', | ||||||
|           command: 'npm run preview', |         url: 'http://127.0.0.1:5050', | ||||||
|           url: 'http://127.0.0.1:5050', |         reuseExistingServer: !isCI, | ||||||
|           reuseExistingServer: true, |       }, | ||||||
|         }, |     } | ||||||
|       }), |   ), | ||||||
| }); | }); | ||||||
|  | |||||||
							
								
								
									
										2828
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2828
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,22 +1,13 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useClipboard, useVModel } from '@vueuse/core'; | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
| 
 | 
 | ||||||
| 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> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
| @ -24,7 +15,7 @@ function onCopyClicked() { | |||||||
|     <template #suffix> |     <template #suffix> | ||||||
|       <n-tooltip trigger="hover"> |       <n-tooltip trigger="hover"> | ||||||
|         <template #trigger> |         <template #trigger> | ||||||
|           <c-button circle variant="text" size="small" @click="onCopyClicked"> |           <c-button circle variant="text" size="small" @click="copy()"> | ||||||
|             <icon-mdi-content-copy /> |             <icon-mdi-content-copy /> | ||||||
|           </c-button> |           </c-button> | ||||||
|         </template> |         </template> | ||||||
|  | |||||||
| @ -1,26 +1,19 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useClipboard } from '@vueuse/core'; | import { useCopy } from '@/composable/copy'; | ||||||
| 
 | 
 | ||||||
| const props = withDefaults(defineProps<{ value?: string }>(), { value: '' }); | const props = withDefaults(defineProps<{ value?: string }>(), { value: '' }); | ||||||
| const { value } = toRefs(props); | const { value } = toRefs(props); | ||||||
| 
 | 
 | ||||||
| const initialText = 'Copy to clipboard'; | const initialText = 'Copy to clipboard'; | ||||||
| const tooltipText = ref(initialText); |  | ||||||
| 
 | 
 | ||||||
| const { copy } = useClipboard({ source: value }); | const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||||
| 
 | const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : initialText); | ||||||
| function handleClick() { |  | ||||||
|   copy(); |  | ||||||
|   tooltipText.value = 'Copied!'; |  | ||||||
| 
 |  | ||||||
|   setTimeout(() => (tooltipText.value = initialText), 1000); |  | ||||||
| } |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <n-tooltip trigger="hover"> |   <n-tooltip trigger="hover"> | ||||||
|     <template #trigger> |     <template #trigger> | ||||||
|       <span class="value" @click="handleClick">{{ value }}</span> |       <span class="value" @click="copy()">{{ value }}</span> | ||||||
|     </template> |     </template> | ||||||
|     {{ tooltipText }} |     {{ tooltipText }} | ||||||
|   </n-tooltip> |   </n-tooltip> | ||||||
|  | |||||||
| @ -1,12 +1,13 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { Copy } from '@vicons/tabler'; | import { Copy } from '@vicons/tabler'; | ||||||
| import { useClipboard, useElementSize } from '@vueuse/core'; | import { useElementSize } from '@vueuse/core'; | ||||||
| import hljs from 'highlight.js/lib/core'; | import hljs from 'highlight.js/lib/core'; | ||||||
| import jsonHljs from 'highlight.js/lib/languages/json'; | import jsonHljs from 'highlight.js/lib/languages/json'; | ||||||
| import sqlHljs from 'highlight.js/lib/languages/sql'; | import sqlHljs from 'highlight.js/lib/languages/sql'; | ||||||
| import xmlHljs from 'highlight.js/lib/languages/xml'; | import xmlHljs from 'highlight.js/lib/languages/xml'; | ||||||
| import yamlHljs from 'highlight.js/lib/languages/yaml'; | import yamlHljs from 'highlight.js/lib/languages/yaml'; | ||||||
| import iniHljs from 'highlight.js/lib/languages/ini'; | import iniHljs from 'highlight.js/lib/languages/ini'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
| 
 | 
 | ||||||
| const props = withDefaults( | const props = withDefaults( | ||||||
|   defineProps<{ |   defineProps<{ | ||||||
| @ -33,17 +34,8 @@ hljs.registerLanguage('toml', iniHljs); | |||||||
| const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); | const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); | ||||||
| const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) }; | const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) }; | ||||||
| 
 | 
 | ||||||
| const { copy } = useClipboard({ source: value }); | const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||||
| const tooltipText = ref(copyMessage.value); | const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.value); | ||||||
| 
 |  | ||||||
| function onCopyClicked() { |  | ||||||
|   copy(); |  | ||||||
|   tooltipText.value = 'Copied !'; |  | ||||||
| 
 |  | ||||||
|   setTimeout(() => { |  | ||||||
|     tooltipText.value = copyMessage.value; |  | ||||||
|   }, 2000); |  | ||||||
| } |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
| @ -61,7 +53,7 @@ function onCopyClicked() { | |||||||
|       <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]"> | ||||||
|             <c-button circle important:h-10 important:w-10 @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" /> | ||||||
|             </c-button> |             </c-button> | ||||||
|           </div> |           </div> | ||||||
| @ -70,7 +62,7 @@ function onCopyClicked() { | |||||||
|       </n-tooltip> |       </n-tooltip> | ||||||
|     </c-card> |     </c-card> | ||||||
|     <div v-if="copyPlacement === 'outside'" mt-4 flex justify-center> |     <div v-if="copyPlacement === 'outside'" mt-4 flex justify-center> | ||||||
|       <c-button @click="onCopyClicked"> |       <c-button @click="copy()"> | ||||||
|         {{ tooltipText }} |         {{ tooltipText }} | ||||||
|       </c-button> |       </c-button> | ||||||
|     </div> |     </div> | ||||||
|  | |||||||
| @ -1,11 +1,19 @@ | |||||||
| import { type MaybeRef, get, useClipboard } from '@vueuse/core'; | // eslint-disable-next-line no-restricted-imports
 | ||||||
|  | import { useClipboard } from '@vueuse/core'; | ||||||
| import { useMessage } from 'naive-ui'; | import { useMessage } from 'naive-ui'; | ||||||
|  | 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?: MaybeRef<unknown>; text?: string } = {}) { |  | ||||||
|   const { copy } = useClipboard(source ? { source: computed(() => String(get(source))) } : {}); |  | ||||||
|   const message = useMessage(); |   const message = useMessage(); | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|  |     ...rest, | ||||||
|  |     isJustCopied: copied, | ||||||
|     async copy(content?: string, { notificationMessage }: { notificationMessage?: string } = {}) { |     async copy(content?: string, { notificationMessage }: { notificationMessage?: string } = {}) { | ||||||
|       if (source) { |       if (source) { | ||||||
|         await copy(); |         await copy(); | ||||||
| @ -14,7 +22,9 @@ export function useCopy({ source, text = 'Copied to the clipboard' }: { source?: | |||||||
|         await copy(content); |         await copy(content); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       message.success(notificationMessage ?? text); |       if (createToast) { | ||||||
|  |         message.success(notificationMessage ?? text); | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -49,7 +49,7 @@ const { t } = useI18n(); | |||||||
|       </transition> |       </transition> | ||||||
| 
 | 
 | ||||||
|       <div v-if="toolStore.newTools.length > 0"> |       <div v-if="toolStore.newTools.length > 0"> | ||||||
|         <n-h3>{{ t('home.categories.newestTools', 'Newest tools') }}</n-h3> |         <n-h3>{{ t('home.categories.newestTools') }}</n-h3> | ||||||
|         <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> |         <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> | ||||||
|           <n-gi v-for="tool in toolStore.newTools" :key="tool.name"> |           <n-gi v-for="tool in toolStore.newTools" :key="tool.name"> | ||||||
|             <ToolCard :tool="tool" /> |             <ToolCard :tool="tool" /> | ||||||
|  | |||||||
| @ -1,6 +1,22 @@ | |||||||
| import type { App } from 'vue'; | import type { Plugin } from 'vue'; | ||||||
| import { createI18n } from 'vue-i18n'; | import { createI18n } from 'vue-i18n'; | ||||||
| import messages from '@intlify/unplugin-vue-i18n/messages'; | 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({ | const i18n = createI18n({ | ||||||
|   legacy: false, |   legacy: false, | ||||||
| @ -8,8 +24,8 @@ const i18n = createI18n({ | |||||||
|   messages, |   messages, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| export const i18nPlugin = { | export const i18nPlugin: Plugin = { | ||||||
|   install: (app: App) => { |   install: (app) => { | ||||||
|     app.use(i18n); |     app.use(i18n); | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										28
									
								
								src/shims.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								src/shims.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -1,21 +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 '~icons/*' { |  | ||||||
|   import { FunctionalComponent, SVGAttributes } from 'vue'; |  | ||||||
|   const component: FunctionalComponent<SVGAttributes>; |  | ||||||
|   export default component; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| declare module 'iarna-toml-esm' { | declare module 'iarna-toml-esm' { | ||||||
|   export const parse: (toml: string) => any; |   export const parse: (toml: string) => any; | ||||||
|   export const stringify: (obj: any) => string; |   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; | ||||||
| } | } | ||||||
| @ -45,7 +45,7 @@ const compareMatch = computed(() => compareSync(compareString.value, compareHash | |||||||
|         <c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text /> |         <c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text /> | ||||||
|       </n-form-item> |       </n-form-item> | ||||||
|       <n-form-item label="Your hash: " label-placement="left"> |       <n-form-item label="Your hash: " label-placement="left"> | ||||||
|         <c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text /> |         <c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text /> | ||||||
|       </n-form-item> |       </n-form-item> | ||||||
|       <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> |       <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> | ||||||
|         <div class="compare-result" :class="{ positive: compareMatch }"> |         <div class="compare-result" :class="{ positive: compareMatch }"> | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ function computeVariance({ data }: { data: number[] }) { | |||||||
|   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 ''; | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -1,10 +1,11 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { Plus, Trash } 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 { arrayToMarkdownTable, computeAverage, computeVariance } 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] }, | ||||||
| @ -47,7 +48,7 @@ const results = computed(() => { | |||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const { copy } = useClipboard(); | const { copy } = useCopy({ createToast: false }); | ||||||
| 
 | 
 | ||||||
| const header = { | const header = { | ||||||
|   title: 'Suite', |   title: 'Suite', | ||||||
|  | |||||||
| @ -92,7 +92,7 @@ const inputLabelAlignmentConfig = { | |||||||
|       v-bind="inputLabelAlignmentConfig" |       v-bind="inputLabelAlignmentConfig" | ||||||
|     /> |     /> | ||||||
| 
 | 
 | ||||||
|     <div divider my-16px /> |     <div my-16px divider /> | ||||||
| 
 | 
 | ||||||
|     <InputCopyable |     <InputCopyable | ||||||
|       v-for="format in formats" |       v-for="format in formats" | ||||||
|  | |||||||
| @ -82,7 +82,7 @@ const formats: DateFormat[] = [ | |||||||
|   { |   { | ||||||
|     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), | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
| @ -146,7 +146,7 @@ function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date | |||||||
|       <c-input-text |       <c-input-text | ||||||
|         v-model:value="inputDate" |         v-model:value="inputDate" | ||||||
|         autofocus |         autofocus | ||||||
|         placeholder="Put you date string here..." |         placeholder="Put your date string here..." | ||||||
|         clearable |         clearable | ||||||
|         test-id="date-time-converter-input" |         test-id="date-time-converter-input" | ||||||
|         :validation="validation" |         :validation="validation" | ||||||
|  | |||||||
| @ -29,7 +29,7 @@ const { copy } = useCopy(); | |||||||
|         Unicode:  <span border="1px solid current op-30" b-rd-xl px-12px py-4px>{{ emojiInfo.unicode }}</span> |         Unicode:  <span border="1px solid current op-30" b-rd-xl px-12px py-4px>{{ emojiInfo.unicode }}</span> | ||||||
|       </div> --> |       </div> --> | ||||||
| 
 | 
 | ||||||
|       <div flex gap-2 font-mono text-xs op-70> |       <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` })"> |         <span cursor-pointer transition hover:text-primary @click="copy(emojiInfo.codePoints, { notificationMessage: `Code points '${emojiInfo.codePoints}' copied to the clipboard` })"> | ||||||
|           {{ emojiInfo.codePoints }} |           {{ emojiInfo.codePoints }} | ||||||
|         </span> |         </span> | ||||||
|  | |||||||
| @ -5,4 +5,4 @@ export type EmojiInfo = { | |||||||
|   emoji: string |   emoji: string | ||||||
|   codePoints: string | undefined |   codePoints: string | undefined | ||||||
|   unicode: string |   unicode: string | ||||||
| } & typeof emojiUnicodeData['\uD83E\uDD10']; | } & typeof emojiUnicodeData[string]; | ||||||
|  | |||||||
| @ -2,6 +2,6 @@ export function convertHexToBin(hex: string) { | |||||||
|   return hex |   return hex | ||||||
|     .trim() |     .trim() | ||||||
|     .split('') |     .split('') | ||||||
|     .map(byte => parseInt(byte, 16).toString(2).padStart(4, '0')) |     .map(byte => Number.parseInt(byte, 16).toString(2).padStart(4, '0')) | ||||||
|     .join(''); |     .join(''); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,14 +1,16 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { format } from 'prettier'; | import { format } from 'prettier'; | ||||||
| import htmlParser from 'prettier/parser-html'; | import htmlParser from 'prettier/plugins/html'; | ||||||
| import { useStorage } from '@vueuse/core'; | import { useStorage } from '@vueuse/core'; | ||||||
| import Editor from './editor/editor.vue'; | import Editor from './editor/editor.vue'; | ||||||
| import TextareaCopyable from '@/components/TextareaCopyable.vue'; | import TextareaCopyable from '@/components/TextareaCopyable.vue'; | ||||||
| 
 | 
 | ||||||
| const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>'); | const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>'); | ||||||
|  | 
 | ||||||
|  | const formattedHtml = asyncComputed(() => format(html.value, { parser: 'html', plugins: [htmlParser] }), ''); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <Editor v-model:html="html" /> |   <Editor v-model:html="html" /> | ||||||
|   <TextareaCopyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" /> |   <TextareaCopyable :value="formattedHtml" language="html" /> | ||||||
| </template> | </template> | ||||||
|  | |||||||
| @ -0,0 +1,52 @@ | |||||||
|  | import { type Page, expect, test } from '@playwright/test'; | ||||||
|  | 
 | ||||||
|  | async function extractIbanInfo({ page }: { page: Page }) { | ||||||
|  |   const itemsLines = await page | ||||||
|  |     .locator('.c-key-value-list__item').all(); | ||||||
|  | 
 | ||||||
|  |   return await Promise.all( | ||||||
|  |     itemsLines.map(async item => [ | ||||||
|  |       (await item.locator('.c-key-value-list__key').textContent() ?? '').trim(), | ||||||
|  |       (await item.locator('.c-key-value-list__value').textContent() ?? '').trim(), | ||||||
|  |     ]), | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | test.describe('Tool - Iban validator and parser', () => { | ||||||
|  |   test.beforeEach(async ({ page }) => { | ||||||
|  |     await page.goto('/iban-validator-and-parser'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('Has correct title', async ({ page }) => { | ||||||
|  |     await expect(page).toHaveTitle('IBAN validator and parser - IT Tools'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('iban info are extracted from a valid iban', async ({ page }) => { | ||||||
|  |     await page.getByTestId('iban-input').fill('DE89370400440532013000'); | ||||||
|  | 
 | ||||||
|  |     const ibanInfo = await extractIbanInfo({ page }); | ||||||
|  | 
 | ||||||
|  |     expect(ibanInfo).toEqual([ | ||||||
|  |       ['Is IBAN valid ?', 'Yes'], | ||||||
|  |       ['Is IBAN a QR-IBAN ?', 'No'], | ||||||
|  |       ['Country code', 'DE'], | ||||||
|  |       ['BBAN', '370400440532013000'], | ||||||
|  |       ['IBAN friendly format', 'DE89 3704 0044 0532 0130 00'], | ||||||
|  |     ]); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('invalid iban errors are displayed', async ({ page }) => { | ||||||
|  |     await page.getByTestId('iban-input').fill('FR7630006060011234567890189'); | ||||||
|  | 
 | ||||||
|  |     const ibanInfo = await extractIbanInfo({ page }); | ||||||
|  | 
 | ||||||
|  |     expect(ibanInfo).toEqual([ | ||||||
|  |       ['Is IBAN valid ?', 'No'], | ||||||
|  |       ['IBAN errors', 'Wrong account bank branch checksum Wrong IBAN checksum'], | ||||||
|  |       ['Is IBAN a QR-IBAN ?', 'No'], | ||||||
|  |       ['Country code', 'N/A'], | ||||||
|  |       ['BBAN', 'N/A'], | ||||||
|  |       ['IBAN friendly format', 'FR76 3000 6060 0112 3456 7890 189'], | ||||||
|  |     ]); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -0,0 +1,18 @@ | |||||||
|  | import { ValidationErrorsIBAN } from 'ibantools'; | ||||||
|  | 
 | ||||||
|  | export { getFriendlyErrors }; | ||||||
|  | 
 | ||||||
|  | const ibanErrorToMessage = { | ||||||
|  |   [ValidationErrorsIBAN.NoIBANProvided]: 'No IBAN provided', | ||||||
|  |   [ValidationErrorsIBAN.NoIBANCountry]: 'No IBAN country', | ||||||
|  |   [ValidationErrorsIBAN.WrongBBANLength]: 'Wrong BBAN length', | ||||||
|  |   [ValidationErrorsIBAN.WrongBBANFormat]: 'Wrong BBAN format', | ||||||
|  |   [ValidationErrorsIBAN.ChecksumNotNumber]: 'Checksum is not a number', | ||||||
|  |   [ValidationErrorsIBAN.WrongIBANChecksum]: 'Wrong IBAN checksum', | ||||||
|  |   [ValidationErrorsIBAN.WrongAccountBankBranchChecksum]: 'Wrong account bank branch checksum', | ||||||
|  |   [ValidationErrorsIBAN.QRIBANNotAllowed]: 'QR-IBAN not allowed', | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function getFriendlyErrors(errorCodes: ValidationErrorsIBAN[]) { | ||||||
|  |   return errorCodes.map(errorCode => ibanErrorToMessage[errorCode]).filter(Boolean); | ||||||
|  | } | ||||||
| @ -0,0 +1,71 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { extractIBAN, friendlyFormatIBAN, isQRIBAN, validateIBAN } from 'ibantools'; | ||||||
|  | import { getFriendlyErrors } from './iban-validator-and-parser.service'; | ||||||
|  | import type { CKeyValueListItems } from '@/ui/c-key-value-list/c-key-value-list.types'; | ||||||
|  | 
 | ||||||
|  | const rawIban = ref(''); | ||||||
|  | 
 | ||||||
|  | const ibanInfo = computed<CKeyValueListItems>(() => { | ||||||
|  |   const iban = rawIban.value.toUpperCase().replace(/\s/g, '').replace(/-/g, ''); | ||||||
|  | 
 | ||||||
|  |   if (iban === '') { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const { valid: isIbanValid, errorCodes } = validateIBAN(iban); | ||||||
|  |   const { countryCode, bban } = extractIBAN(iban); | ||||||
|  |   const errors = getFriendlyErrors(errorCodes); | ||||||
|  | 
 | ||||||
|  |   return [ | ||||||
|  | 
 | ||||||
|  |     { | ||||||
|  |       label: 'Is IBAN valid ?', | ||||||
|  |       value: isIbanValid, | ||||||
|  |       showCopyButton: false, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       label: 'IBAN errors', | ||||||
|  |       value: errors.length === 0 ? undefined : errors, | ||||||
|  |       hideOnNil: true, | ||||||
|  |       showCopyButton: false, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       label: 'Is IBAN a QR-IBAN ?', | ||||||
|  |       value: isQRIBAN(iban), | ||||||
|  |       showCopyButton: false, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       label: 'Country code', | ||||||
|  |       value: countryCode, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       label: 'BBAN', | ||||||
|  |       value: bban, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       label: 'IBAN friendly format', | ||||||
|  |       value: friendlyFormatIBAN(iban), | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const ibanExamples = [ | ||||||
|  |   'FR7630006000011234567890189', | ||||||
|  |   'DE89370400440532013000', | ||||||
|  |   'GB29NWBK60161331926819', | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-input-text v-model:value="rawIban" placeholder="Enter an IBAN to check for validity..." test-id="iban-input" /> | ||||||
|  | 
 | ||||||
|  |     <c-key-value-list :items="ibanInfo" my-5 data-test-id="iban-info" /> | ||||||
|  | 
 | ||||||
|  |     <c-card title="Valid IBAN examples"> | ||||||
|  |       <div v-for="iban in ibanExamples" :key="iban"> | ||||||
|  |         <c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" /> | ||||||
|  |       </div> | ||||||
|  |     </c-card> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
							
								
								
									
										12
									
								
								src/tools/iban-validator-and-parser/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/iban-validator-and-parser/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | import { defineTool } from '../tool'; | ||||||
|  | import Bank from '~icons/mdi/bank'; | ||||||
|  | 
 | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'IBAN validator and parser', | ||||||
|  |   path: '/iban-validator-and-parser', | ||||||
|  |   description: 'Validate and parse IBAN numbers. Check if IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format.', | ||||||
|  |   keywords: ['iban', 'validator', 'and', 'parser', 'bic', 'bank'], | ||||||
|  |   component: () => import('./iban-validator-and-parser.vue'), | ||||||
|  |   icon: Bank, | ||||||
|  |   createdAt: new Date('2023-08-26'), | ||||||
|  | }); | ||||||
| @ -1,6 +1,10 @@ | |||||||
| import { tool as base64FileConverter } from './base64-file-converter'; | import { tool as base64FileConverter } from './base64-file-converter'; | ||||||
| import { tool as base64StringConverter } from './base64-string-converter'; | import { tool as base64StringConverter } from './base64-string-converter'; | ||||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | import { tool as basicAuthGenerator } from './basic-auth-generator'; | ||||||
|  | import { tool as ulidGenerator } from './ulid-generator'; | ||||||
|  | import { tool as ibanValidatorAndParser } from './iban-validator-and-parser'; | ||||||
|  | import { tool as stringObfuscator } from './string-obfuscator'; | ||||||
|  | import { tool as textDiff } from './text-diff'; | ||||||
| import { tool as emojiPicker } from './emoji-picker'; | import { tool as emojiPicker } from './emoji-picker'; | ||||||
| import { tool as passwordStrengthAnalyser } from './password-strength-analyser'; | import { tool as passwordStrengthAnalyser } from './password-strength-analyser'; | ||||||
| import { tool as yamlToToml } from './yaml-to-toml'; | import { tool as yamlToToml } from './yaml-to-toml'; | ||||||
| @ -53,6 +57,7 @@ import { tool as metaTagGenerator } from './meta-tag-generator'; | |||||||
| import { tool as mimeTypes } from './mime-types'; | import { tool as mimeTypes } from './mime-types'; | ||||||
| import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator'; | import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator'; | ||||||
| import { tool as qrCodeGenerator } from './qr-code-generator'; | import { tool as qrCodeGenerator } from './qr-code-generator'; | ||||||
|  | import { tool as wifiQrCodeGenerator } from './wifi-qr-code-generator'; | ||||||
| import { tool as randomPortGenerator } from './random-port-generator'; | import { tool as randomPortGenerator } from './random-port-generator'; | ||||||
| import { tool as romanNumeralConverter } from './roman-numeral-converter'; | import { tool as romanNumeralConverter } from './roman-numeral-converter'; | ||||||
| import { tool as sqlPrettify } from './sql-prettify'; | import { tool as sqlPrettify } from './sql-prettify'; | ||||||
| @ -70,7 +75,7 @@ import { tool as xmlFormatter } from './xml-formatter'; | |||||||
| export const toolsByCategory: ToolCategory[] = [ | export const toolsByCategory: ToolCategory[] = [ | ||||||
|   { |   { | ||||||
|     name: 'Crypto', |     name: 'Crypto', | ||||||
|     components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser], |     components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Converter', |     name: 'Converter', | ||||||
| @ -114,7 +119,7 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Images and videos', |     name: 'Images and videos', | ||||||
|     components: [qrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], |     components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Development', |     name: 'Development', | ||||||
| @ -145,11 +150,11 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Text', |     name: 'Text', | ||||||
|     components: [loremIpsumGenerator, textStatistics, emojiPicker], |     components: [loremIpsumGenerator, textStatistics, emojiPicker, stringObfuscator, textDiff], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Data', |     name: 'Data', | ||||||
|     components: [phoneParserAndFormatter], |     components: [phoneParserAndFormatter, ibanValidatorAndParser], | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -23,7 +23,7 @@ function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: str | |||||||
|     + _.chain(ip) |     + _.chain(ip) | ||||||
|       .trim() |       .trim() | ||||||
|       .split('.') |       .split('.') | ||||||
|       .map(part => parseInt(part).toString(16).padStart(2, '0')) |       .map(part => Number.parseInt(part).toString(16).padStart(2, '0')) | ||||||
|       .chunk(2) |       .chunk(2) | ||||||
|       .map(blocks => blocks.join('')) |       .map(blocks => blocks.join('')) | ||||||
|       .join(':') |       .join(':') | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ function getRangesize(start: string, end: string) { | |||||||
|     return -1; |     return -1; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return 1 + parseInt(end, 2) - parseInt(start, 2); |   return 1 + Number.parseInt(end, 2) - Number.parseInt(start, 2); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getCidr(start: string, end: string) { | function getCidr(start: string, end: string) { | ||||||
| @ -55,8 +55,8 @@ function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) { | |||||||
|   const cidr = getCidr(start, end); |   const cidr = getCidr(start, end); | ||||||
|   if (cidr != null) { |   if (cidr != null) { | ||||||
|     const result: Ipv4RangeExpanderResult = {}; |     const result: Ipv4RangeExpanderResult = {}; | ||||||
|     result.newEnd = bits2ip(parseInt(cidr.end, 2)); |     result.newEnd = bits2ip(Number.parseInt(cidr.end, 2)); | ||||||
|     result.newStart = bits2ip(parseInt(cidr.start, 2)); |     result.newStart = bits2ip(Number.parseInt(cidr.start, 2)); | ||||||
|     result.newCidr = `${result.newStart}/${cidr.mask}`; |     result.newCidr = `${result.newStart}/${cidr.mask}`; | ||||||
|     result.newSize = getRangesize(cidr.start, cidr.end); |     result.newSize = getRangesize(cidr.start, cidr.end); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| import jwtDecode, { type JwtHeader, type JwtPayload } from 'jwt-decode'; | import jwtDecode, { type JwtHeader, type JwtPayload } from 'jwt-decode'; | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| import { match } from 'ts-pattern'; |  | ||||||
| import { ALGORITHM_DESCRIPTIONS, CLAIM_DESCRIPTIONS } from './jwt-parser.constants'; | import { ALGORITHM_DESCRIPTIONS, CLAIM_DESCRIPTIONS } from './jwt-parser.constants'; | ||||||
| 
 | 
 | ||||||
| export { decodeJwt }; | export { decodeJwt }; | ||||||
| @ -32,10 +31,15 @@ function parseClaims({ claim, value }: { claim: string; value: unknown }) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) { | function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) { | ||||||
|   return match(claim) |   if (['exp', 'nbf', 'iat'].includes(claim)) { | ||||||
|     .with('exp', 'nbf', 'iat', () => dateFormatter(value)) |     return dateFormatter(value); | ||||||
|     .with('alg', () => (_.isString(value) ? ALGORITHM_DESCRIPTIONS[value] : undefined)) |   } | ||||||
|     .otherwise(() => undefined); | 
 | ||||||
|  |   if (claim === 'alg' && _.isString(value)) { | ||||||
|  |     return ALGORITHM_DESCRIPTIONS[value]; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return undefined; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function dateFormatter(value: unknown) { | function dateFormatter(value: unknown) { | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '') | |||||||
| const macAddress = ref('20:37:06:12:34:56'); | const macAddress = ref('20:37:06:12:34:56'); | ||||||
| const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]); | const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]); | ||||||
| 
 | 
 | ||||||
| const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' }); | const { copy } = useCopy({ source: () => details.value ?? '', text: 'Vendor info copied to the clipboard' }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ export { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function hexToBytes(hex: string) { | function hexToBytes(hex: string) { | ||||||
|   return (hex.match(/.{1,2}/g) ?? []).map(char => parseInt(char, 16)); |   return (hex.match(/.{1,2}/g) ?? []).map(char => Number.parseInt(char, 16)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function computeHMACSha1(message: string, key: string) { | function computeHMACSha1(message: string, key: string) { | ||||||
| @ -32,7 +32,7 @@ function base32toHex(base32: string) { | |||||||
|     .map(value => base32Chars.indexOf(value).toString(2).padStart(5, '0')) |     .map(value => base32Chars.indexOf(value).toString(2).padStart(5, '0')) | ||||||
|     .join(''); |     .join(''); | ||||||
| 
 | 
 | ||||||
|   const hex = (bits.match(/.{1,8}/g) ?? []).map(chunk => parseInt(chunk, 2).toString(16).padStart(2, '0')).join(''); |   const hex = (bits.match(/.{1,8}/g) ?? []).map(chunk => Number.parseInt(chunk, 2).toString(16).padStart(2, '0')).join(''); | ||||||
| 
 | 
 | ||||||
|   return hex; |   return hex; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,10 +1,10 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useClipboard } from '@vueuse/core'; | import { useCopy } from '@/composable/copy'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>(); | const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>(); | ||||||
| const { copy: copyPrevious, copied: previousCopied } = useClipboard(); | const { copy: copyPrevious, isJustCopied: previousCopied } = useCopy({ createToast: false }); | ||||||
| const { copy: copyCurrent, copied: currentCopied } = useClipboard(); | const { copy: copyCurrent, isJustCopied: currentCopied } = useCopy({ createToast: false }); | ||||||
| const { copy: copyNext, copied: nextCopied } = useClipboard(); | const { copy: copyNext, isJustCopied: nextCopied } = useCopy({ createToast: false }); | ||||||
| 
 | 
 | ||||||
| const { tokens } = toRefs(props); | const { tokens } = toRefs(props); | ||||||
| </script> | </script> | ||||||
|  | |||||||
| @ -14,6 +14,6 @@ test.describe('Tool - Password strength analyser', () => { | |||||||
| 
 | 
 | ||||||
|     const crackDuration = await page.getByTestId('crack-duration').textContent(); |     const crackDuration = await page.getByTestId('crack-duration').textContent(); | ||||||
| 
 | 
 | ||||||
|     expect(crackDuration).toEqual('15,091 milleniums, 3 centurys'); |     expect(crackDuration).toEqual('15,091 millennia, 3 centuries'); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ export { getPasswordCrackTimeEstimation, getCharsetLength }; | |||||||
| 
 | 
 | ||||||
| function prettifyExponentialNotation(exponentialNotation: number) { | function prettifyExponentialNotation(exponentialNotation: number) { | ||||||
|   const [base, exponent] = exponentialNotation.toString().split('e'); |   const [base, exponent] = exponentialNotation.toString().split('e'); | ||||||
|   const baseAsNumber = parseFloat(base); |   const baseAsNumber = Number.parseFloat(base); | ||||||
|   const prettyBase = baseAsNumber % 1 === 0 ? baseAsNumber.toLocaleString() : baseAsNumber.toFixed(2); |   const prettyBase = baseAsNumber % 1 === 0 ? baseAsNumber.toLocaleString() : baseAsNumber.toFixed(2); | ||||||
|   return exponent ? `${prettyBase}e${exponent}` : prettyBase; |   return exponent ? `${prettyBase}e${exponent}` : prettyBase; | ||||||
| } | } | ||||||
| @ -19,20 +19,20 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const timeUnits = [ |   const timeUnits = [ | ||||||
|     { unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation }, |     { unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation, plural: 'millennia' }, | ||||||
|     { unit: 'century', secondsInUnit: 3153600000 }, |     { unit: 'century', secondsInUnit: 3153600000, plural: 'centuries' }, | ||||||
|     { unit: 'decade', secondsInUnit: 315360000 }, |     { unit: 'decade', secondsInUnit: 315360000, plural: 'decades' }, | ||||||
|     { unit: 'year', secondsInUnit: 31536000 }, |     { unit: 'year', secondsInUnit: 31536000, plural: 'years' }, | ||||||
|     { unit: 'month', secondsInUnit: 2592000 }, |     { unit: 'month', secondsInUnit: 2592000, plural: 'months' }, | ||||||
|     { unit: 'week', secondsInUnit: 604800 }, |     { unit: 'week', secondsInUnit: 604800, plural: 'weeks' }, | ||||||
|     { unit: 'day', secondsInUnit: 86400 }, |     { unit: 'day', secondsInUnit: 86400, plural: 'days' }, | ||||||
|     { unit: 'hour', secondsInUnit: 3600 }, |     { unit: 'hour', secondsInUnit: 3600, plural: 'hours' }, | ||||||
|     { unit: 'minute', secondsInUnit: 60 }, |     { unit: 'minute', secondsInUnit: 60, plural: 'minutes' }, | ||||||
|     { unit: 'second', secondsInUnit: 1 }, |     { unit: 'second', secondsInUnit: 1, plural: 'seconds' }, | ||||||
|   ]; |   ]; | ||||||
| 
 | 
 | ||||||
|   return _.chain(timeUnits) |   return _.chain(timeUnits) | ||||||
|     .map(({ unit, secondsInUnit, format = _.identity }) => { |     .map(({ unit, secondsInUnit, plural, format = _.identity }) => { | ||||||
|       const quantity = Math.floor(seconds / secondsInUnit); |       const quantity = Math.floor(seconds / secondsInUnit); | ||||||
|       seconds %= secondsInUnit; |       seconds %= secondsInUnit; | ||||||
| 
 | 
 | ||||||
| @ -41,7 +41,7 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const formattedQuantity = format(quantity); |       const formattedQuantity = format(quantity); | ||||||
|       return `${formattedQuantity} ${unit}${quantity > 1 ? 's' : ''}`; |       return `${formattedQuantity} ${quantity > 1 ? plural : unit}`; | ||||||
|     }) |     }) | ||||||
|     .compact() |     .compact() | ||||||
|     .take(2) |     .take(2) | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ const validationRoman = useValidation({ | |||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const { copy: copyRoman } = useCopy({ source: outputRoman, text: 'Roman number copied to the clipboard' }); | const { copy: copyRoman } = useCopy({ source: outputRoman, text: 'Roman number copied to the clipboard' }); | ||||||
| const { copy: copyArabic } = useCopy({ source: outputNumeral, text: 'Arabic number copied to the clipboard' }); | const { copy: copyArabic } = useCopy({ source: () => String(outputNumeral), text: 'Arabic number copied to the clipboard' }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | |||||||
| @ -1,11 +1,11 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { type FormatFnOptions, format as formatSQL } from 'sql-formatter'; | import { type FormatOptionsWithLanguage, format as formatSQL } from 'sql-formatter'; | ||||||
| import TextareaCopyable from '@/components/TextareaCopyable.vue'; | import TextareaCopyable from '@/components/TextareaCopyable.vue'; | ||||||
| import { useStyleStore } from '@/stores/style.store'; | import { useStyleStore } from '@/stores/style.store'; | ||||||
| 
 | 
 | ||||||
| const inputElement = ref<HTMLElement>(); | const inputElement = ref<HTMLElement>(); | ||||||
| const styleStore = useStyleStore(); | const styleStore = useStyleStore(); | ||||||
| const config = reactive<Partial<FormatFnOptions>>({ | const config = reactive<FormatOptionsWithLanguage>({ | ||||||
|   keywordCase: 'upper', |   keywordCase: 'upper', | ||||||
|   useTabs: false, |   useTabs: false, | ||||||
|   language: 'sql', |   language: 'sql', | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								src/tools/string-obfuscator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/string-obfuscator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | import { EyeOff } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  | 
 | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'String obfuscator', | ||||||
|  |   path: '/string-obfuscator', | ||||||
|  |   description: 'Obfuscate a string (like a secret, an IBAN, or a token) to make it shareable and identifiable without revealing its content.', | ||||||
|  |   keywords: ['string', 'obfuscator', 'secret', 'token', 'hide', 'obscure', 'mask', 'masking'], | ||||||
|  |   component: () => import('./string-obfuscator.vue'), | ||||||
|  |   icon: EyeOff, | ||||||
|  |   createdAt: new Date('2023-08-16'), | ||||||
|  | }); | ||||||
							
								
								
									
										20
									
								
								src/tools/string-obfuscator/string-obfuscator.model.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/tools/string-obfuscator/string-obfuscator.model.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | import { describe, expect, it } from 'vitest'; | ||||||
|  | import { obfuscateString } from './string-obfuscator.model'; | ||||||
|  | 
 | ||||||
|  | describe('string-obfuscator model', () => { | ||||||
|  |   describe('obfuscateString', () => { | ||||||
|  |     it('the characters in the middle of the string are replaced by the replacement character', () => { | ||||||
|  |       expect(obfuscateString('1234567890')).toBe('1234******'); | ||||||
|  |       expect(obfuscateString('1234567890', { replacementChar: 'x' })).toBe('1234xxxxxx'); | ||||||
|  |       expect(obfuscateString('1234567890', { keepFirst: 5 })).toBe('12345*****'); | ||||||
|  |       expect(obfuscateString('1234567890', { keepFirst: 0, keepLast: 5 })).toBe('*****67890'); | ||||||
|  |       expect(obfuscateString('1234567890', { keepFirst: 5, keepLast: 5 })).toBe('1234567890'); | ||||||
|  |       expect(obfuscateString('1234567890', { keepFirst: 2, keepLast: 2, replacementChar: 'x' })).toBe('12xxxxxx90'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('by default, the spaces are kept, they can be removed with the keepSpace option', () => { | ||||||
|  |       expect(obfuscateString('12345 67890')).toBe('1234* *****'); | ||||||
|  |       expect(obfuscateString('12345 67890', { keepSpace: false })).toBe('1234*******'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										35
									
								
								src/tools/string-obfuscator/string-obfuscator.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/tools/string-obfuscator/string-obfuscator.model.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | import { get } from '@vueuse/core'; | ||||||
|  | import { type MaybeRef, computed } from 'vue'; | ||||||
|  | 
 | ||||||
|  | export { obfuscateString, useObfuscateString }; | ||||||
|  | 
 | ||||||
|  | function obfuscateString( | ||||||
|  |   str: string, | ||||||
|  |   { replacementChar = '*', keepFirst = 4, keepLast = 0, keepSpace = true }: { replacementChar?: string; keepFirst?: number; keepLast?: number; keepSpace?: boolean } = {}): string { | ||||||
|  |   return str | ||||||
|  |     .split('') | ||||||
|  |     .map((char, index, array) => { | ||||||
|  |       if (keepSpace && char === ' ') { | ||||||
|  |         return char; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return (index < keepFirst || index >= array.length - keepLast) ? char : replacementChar; | ||||||
|  |     }) | ||||||
|  |     .join(''); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function useObfuscateString( | ||||||
|  |   str: MaybeRef<string>, | ||||||
|  |   config: { replacementChar?: MaybeRef<string>; keepFirst?: MaybeRef<number>; keepLast?: MaybeRef<number>; keepSpace?: MaybeRef<boolean> } = {}, | ||||||
|  | 
 | ||||||
|  | ) { | ||||||
|  |   return computed(() => obfuscateString( | ||||||
|  |     get(str), | ||||||
|  |     { | ||||||
|  |       replacementChar: get(config.replacementChar), | ||||||
|  |       keepFirst: get(config.keepFirst), | ||||||
|  |       keepLast: get(config.keepLast), | ||||||
|  |       keepSpace: get(config.keepSpace), | ||||||
|  |     }, | ||||||
|  |   )); | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								src/tools/string-obfuscator/string-obfuscator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/tools/string-obfuscator/string-obfuscator.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useObfuscateString } from './string-obfuscator.model'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  | 
 | ||||||
|  | const str = ref('Lorem ipsum dolor sit amet'); | ||||||
|  | const keepFirst = ref(4); | ||||||
|  | const keepLast = ref(4); | ||||||
|  | const keepSpace = ref(true); | ||||||
|  | 
 | ||||||
|  | const obfuscatedString = useObfuscateString(str, { keepFirst, keepLast, keepSpace }); | ||||||
|  | const { copy } = useCopy({ source: obfuscatedString }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-input-text v-model:value="str" raw-text placeholder="Enter string to obfuscate" label="String to obfuscate:" clearable multiline /> | ||||||
|  | 
 | ||||||
|  |     <div mt-4 flex gap-10px> | ||||||
|  |       <div> | ||||||
|  |         <div>Keep first:</div> | ||||||
|  |         <n-input-number v-model:value="keepFirst" min="0" /> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <div> | ||||||
|  |         <div>Keep last:</div> | ||||||
|  |         <n-input-number v-model:value="keepLast" min="0" /> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <div> | ||||||
|  |         <div mb-5px> | ||||||
|  |           Keep spaces: | ||||||
|  |         </div> | ||||||
|  |         <n-switch v-model:value="keepSpace" /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <c-card v-if="obfuscatedString" mt-60px max-w-600px flex items-center gap-5px font-mono> | ||||||
|  |       <div break-anywhere text-wrap> | ||||||
|  |         {{ obfuscatedString }} | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <c-button @click="copy()"> | ||||||
|  |         <icon-mdi:content-copy /> | ||||||
|  |       </c-button> | ||||||
|  |     </c-card> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
							
								
								
									
										12
									
								
								src/tools/text-diff/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/text-diff/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | import { FileDiff } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  | 
 | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'Text diff', | ||||||
|  |   path: '/text-diff', | ||||||
|  |   description: 'Compare two texts and see the differences between them.', | ||||||
|  |   keywords: ['text', 'diff', 'compare', 'string', 'text diff', 'code'], | ||||||
|  |   component: () => import('./text-diff.vue'), | ||||||
|  |   icon: FileDiff, | ||||||
|  |   createdAt: new Date('2023-08-16'), | ||||||
|  | }); | ||||||
							
								
								
									
										5
									
								
								src/tools/text-diff/text-diff.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/tools/text-diff/text-diff.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | <template> | ||||||
|  |   <c-card w-full important:flex-1 important:pa-0> | ||||||
|  |     <c-diff-editor /> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
| @ -15,14 +15,12 @@ export function createToken({ | |||||||
|   length?: number |   length?: number | ||||||
|   alphabet?: string |   alphabet?: string | ||||||
| }) { | }) { | ||||||
|   const allAlphabet |   const allAlphabet = alphabet ?? [ | ||||||
|     = alphabet |     withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : '', | ||||||
|     ?? [ |     withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '', | ||||||
|       ...(withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : ''), |     withNumbers ? '0123456789' : '', | ||||||
|       ...(withLowercase ? 'abcdefghijklmopqrstuvwxyz' : ''), |     withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '', | ||||||
|       ...(withNumbers ? '0123456789' : ''), |   ].join(''); ; | ||||||
|       ...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : ''), |  | ||||||
|     ].join(''); |  | ||||||
| 
 | 
 | ||||||
|   return shuffleString(allAlphabet.repeat(length)).substring(0, length); |   return shuffleString(allAlphabet.repeat(length)).substring(0, length); | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								src/tools/ulid-generator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/ulid-generator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | import { SortDescendingNumbers } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  | 
 | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'ULID generator', | ||||||
|  |   path: '/ulid-generator', | ||||||
|  |   description: 'Generate random Universally Unique Lexicographically Sortable Identifier (ULID).', | ||||||
|  |   keywords: ['ulid', 'generator', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'], | ||||||
|  |   component: () => import('./ulid-generator.vue'), | ||||||
|  |   icon: SortDescendingNumbers, | ||||||
|  |   createdAt: new Date('2023-09-11'), | ||||||
|  | }); | ||||||
							
								
								
									
										23
									
								
								src/tools/ulid-generator/ulid-generator.e2e.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/tools/ulid-generator/ulid-generator.e2e.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | import { expect, test } from '@playwright/test'; | ||||||
|  | 
 | ||||||
|  | const ULID_REGEX = /[0-9A-Z]{26}/; | ||||||
|  | 
 | ||||||
|  | test.describe('Tool - ULID generator', () => { | ||||||
|  |   test.beforeEach(async ({ page }) => { | ||||||
|  |     await page.goto('/ulid-generator'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('Has correct title', async ({ page }) => { | ||||||
|  |     await expect(page).toHaveTitle('ULID generator - IT Tools'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('the refresh button generates a new ulid', async ({ page }) => { | ||||||
|  |     const ulid = await page.getByTestId('ulids').textContent(); | ||||||
|  |     expect(ulid?.trim()).toMatch(ULID_REGEX); | ||||||
|  | 
 | ||||||
|  |     await page.getByTestId('refresh').click(); | ||||||
|  |     const newUlid = await page.getByTestId('ulids').textContent(); | ||||||
|  |     expect(ulid?.trim()).not.toBe(newUlid?.trim()); | ||||||
|  |     expect(newUlid?.trim()).toMatch(ULID_REGEX); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										46
									
								
								src/tools/ulid-generator/ulid-generator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/tools/ulid-generator/ulid-generator.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { ulid } from 'ulid'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import { computedRefreshable } from '@/composable/computedRefreshable'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  | 
 | ||||||
|  | const amount = useStorage('ulid-generator-amount', 1); | ||||||
|  | const formats = [{ label: 'Raw', value: 'raw' }, { label: 'JSON', value: 'json' }] as const; | ||||||
|  | const format = useStorage<typeof formats[number]['value']>('ulid-generator-format', formats[0].value); | ||||||
|  | 
 | ||||||
|  | const [ulids, refreshUlids] = computedRefreshable(() => { | ||||||
|  |   const ids = _.times(amount.value, () => ulid()); | ||||||
|  | 
 | ||||||
|  |   if (format.value === 'json') { | ||||||
|  |     return JSON.stringify(ids, null, 2); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ids.join('\n'); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const { copy } = useCopy({ source: ulids, text: 'ULIDs copied to the clipboard' }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div flex flex-col justify-center gap-2> | ||||||
|  |     <div flex items-center> | ||||||
|  |       <label w-75px> Quantity:</label> | ||||||
|  |       <n-input-number v-model:value="amount" min="1" max="100" flex-1 /> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <c-buttons-select v-model:value="format" :options="formats" label="Format: " label-width="75px" /> | ||||||
|  | 
 | ||||||
|  |     <c-card mt-5 flex data-test-id="ulids"> | ||||||
|  |       <pre m-0 m-x-auto>{{ ulids }}</pre> | ||||||
|  |     </c-card> | ||||||
|  | 
 | ||||||
|  |     <div flex justify-center gap-2> | ||||||
|  |       <c-button data-test-id="refresh" @click="refreshUlids()"> | ||||||
|  |         Refresh | ||||||
|  |       </c-button> | ||||||
|  |       <c-button @click="copy()"> | ||||||
|  |         Copy | ||||||
|  |       </c-button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
| @ -5,7 +5,7 @@ export const tool = defineTool({ | |||||||
|   name: 'UUIDs v4 generator', |   name: 'UUIDs v4 generator', | ||||||
|   path: '/uuid-generator', |   path: '/uuid-generator', | ||||||
|   description: |   description: | ||||||
|     'A universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot !).', |     'A Universally Unique Identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot!).', | ||||||
|   keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'], |   keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'], | ||||||
|   component: () => import('./uuid-generator.vue'), |   component: () => import('./uuid-generator.vue'), | ||||||
|   icon: Fingerprint, |   icon: Fingerprint, | ||||||
|  | |||||||
| @ -34,7 +34,7 @@ const { copy } = useCopy({ source: uuids, text: 'UUIDs copied to the clipboard' | |||||||
|     /> |     /> | ||||||
| 
 | 
 | ||||||
|     <div flex justify-center gap-3> |     <div flex justify-center gap-3> | ||||||
|       <c-button autofocus @click="copy"> |       <c-button autofocus @click="copy()"> | ||||||
|         Copy |         Copy | ||||||
|       </c-button> |       </c-button> | ||||||
|       <c-button @click="refreshUUIDs"> |       <c-button @click="refreshUUIDs"> | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								src/tools/wifi-qr-code-generator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/tools/wifi-qr-code-generator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | import { Qrcode } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  | 
 | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'WiFi QR Code generator', | ||||||
|  |   path: '/wifi-qrcode-generator', | ||||||
|  |   description: | ||||||
|  |     'Generate and download QR-codes for quick connections to WiFi networks.', | ||||||
|  |   keywords: ['qr', 'code', 'generator', 'square', 'color', 'link', 'low', 'medium', 'quartile', 'high', 'transparent', 'wifi'], | ||||||
|  |   component: () => import('./wifi-qr-code-generator.vue'), | ||||||
|  |   icon: Qrcode, | ||||||
|  |   createdAt: new Date('2023-09-06'), | ||||||
|  | }); | ||||||
							
								
								
									
										146
									
								
								src/tools/wifi-qr-code-generator/useQRCode.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/tools/wifi-qr-code-generator/useQRCode.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,146 @@ | |||||||
|  | import { type MaybeRef, get } from '@vueuse/core'; | ||||||
|  | import QRCode, { type QRCodeToDataURLOptions } from 'qrcode'; | ||||||
|  | import { isRef, ref, watch } from 'vue'; | ||||||
|  | 
 | ||||||
|  | export const wifiEncryptions = ['WEP', 'WPA', 'nopass', 'WPA2-EAP'] as const; | ||||||
|  | export type WifiEncryption = typeof wifiEncryptions[number]; | ||||||
|  | 
 | ||||||
|  | // @see https://en.wikipedia.org/wiki/Extensible_Authentication_Protocol
 | ||||||
|  | // for a list of available EAP methods. There are a lot (40!) of them.
 | ||||||
|  | export const EAPMethods = [ | ||||||
|  |   'MD5', | ||||||
|  |   'POTP', | ||||||
|  |   'GTC', | ||||||
|  |   'TLS', | ||||||
|  |   'IKEv2', | ||||||
|  |   'SIM', | ||||||
|  |   'AKA', | ||||||
|  |   'AKA\'', | ||||||
|  |   'TTLS', | ||||||
|  |   'PWD', | ||||||
|  |   'LEAP', | ||||||
|  |   'PSK', | ||||||
|  |   'FAST', | ||||||
|  |   'TEAP', | ||||||
|  |   'EKE', | ||||||
|  |   'NOOB', | ||||||
|  |   'PEAP', | ||||||
|  | ] as const; | ||||||
|  | export type EAPMethod = typeof EAPMethods[number]; | ||||||
|  | 
 | ||||||
|  | export const EAPPhase2Methods = [ | ||||||
|  |   'None', | ||||||
|  |   'MSCHAPV2', | ||||||
|  | ] as const; | ||||||
|  | export type EAPPhase2Method = typeof EAPPhase2Methods[number]; | ||||||
|  | 
 | ||||||
|  | interface IWifiQRCodeOptions { | ||||||
|  |   ssid: MaybeRef<string> | ||||||
|  |   password: MaybeRef<string> | ||||||
|  |   eapMethod: MaybeRef<EAPMethod> | ||||||
|  |   isHiddenSSID: MaybeRef<boolean> | ||||||
|  |   eapAnonymous: MaybeRef<boolean> | ||||||
|  |   eapIdentity: MaybeRef<string> | ||||||
|  |   eapPhase2Method: MaybeRef<EAPPhase2Method> | ||||||
|  |   color: { foreground: MaybeRef<string>; background: MaybeRef<string> } | ||||||
|  |   options?: QRCodeToDataURLOptions | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface GetQrCodeTextOptions { | ||||||
|  |   ssid: string | ||||||
|  |   password: string | ||||||
|  |   encryption: WifiEncryption | ||||||
|  |   eapMethod: EAPMethod | ||||||
|  |   isHiddenSSID: boolean | ||||||
|  |   eapAnonymous: boolean | ||||||
|  |   eapIdentity: string | ||||||
|  |   eapPhase2Method: EAPPhase2Method | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function escapeString(str: string) { | ||||||
|  |   // replaces \, ;, ,, " and : with the same character preceded by a backslash
 | ||||||
|  |   return str.replace(/([\\;,:"])/g, '\\$1'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getQrCodeText(options: GetQrCodeTextOptions): string | null { | ||||||
|  |   const { ssid, password, encryption, eapMethod, isHiddenSSID, eapAnonymous, eapIdentity, eapPhase2Method } = options; | ||||||
|  |   if (!ssid) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |   if (encryption === 'nopass') { | ||||||
|  |     return `WIFI:S:${escapeString(ssid)};;`; // type can be omitted in that case, and password is not needed, makes the QR Code smaller
 | ||||||
|  |   } | ||||||
|  |   if (encryption !== 'WPA2-EAP' && password) { | ||||||
|  |     // EAP has a lot of options, so we'll handle it separately
 | ||||||
|  |     // WPA and WEP are pretty simple though.
 | ||||||
|  |     return `WIFI:S:${escapeString(ssid)};T:${encryption};P:${escapeString(password)};${isHiddenSSID ? 'H:true' : ''};`; | ||||||
|  |   } | ||||||
|  |   if (encryption === 'WPA2-EAP' && password && eapMethod) { | ||||||
|  |     // WPA2-EAP string is a lot more complex, first off, we drop the text if there is no identity, and it's not anonymous.
 | ||||||
|  |     if (!eapIdentity && !eapAnonymous) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     // From reading, I could only find that a phase 2 is required for the PEAP method, I may be wrong though, I didn't read the whole spec.
 | ||||||
|  |     if (eapMethod === 'PEAP' && !eapPhase2Method) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     // The string is built in the following order:
 | ||||||
|  |     // 1. SSID
 | ||||||
|  |     // 2. Authentication type
 | ||||||
|  |     // 3. Password
 | ||||||
|  |     // 4. EAP method
 | ||||||
|  |     // 5. EAP phase 2 method
 | ||||||
|  |     // 6. Identity or anonymous if checked
 | ||||||
|  |     // 7. Hidden SSID if checked
 | ||||||
|  |     const identity = eapAnonymous ? 'A:anon' : `I:${escapeString(eapIdentity)}`; | ||||||
|  |     const phase2 = eapPhase2Method !== 'None' ? `PH2:${eapPhase2Method};` : ''; | ||||||
|  |     return `WIFI:S:${escapeString(ssid)};T:WPA2-EAP;P:${escapeString(password)};E:${eapMethod};${phase2}${identity};${isHiddenSSID ? 'H:true' : ''};`; | ||||||
|  |   } | ||||||
|  |   return null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useWifiQRCode({ | ||||||
|  |   ssid, | ||||||
|  |   password, | ||||||
|  |   eapMethod, | ||||||
|  |   isHiddenSSID, | ||||||
|  |   eapAnonymous, | ||||||
|  |   eapIdentity, | ||||||
|  |   eapPhase2Method, | ||||||
|  |   color: { background, foreground }, | ||||||
|  |   options, | ||||||
|  | }: IWifiQRCodeOptions) { | ||||||
|  |   const qrcode = ref(''); | ||||||
|  |   const encryption = ref<WifiEncryption>('WPA'); | ||||||
|  | 
 | ||||||
|  |   watch( | ||||||
|  |     [ssid, password, encryption, eapMethod, isHiddenSSID, eapAnonymous, eapIdentity, eapPhase2Method, background, foreground].filter(isRef), | ||||||
|  |     async () => { | ||||||
|  |       // @see https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
 | ||||||
|  |       // This is the full spec, there's quite a bit of logic to generate the string embeddedin the QR code.
 | ||||||
|  |       const text = getQrCodeText({ | ||||||
|  |         ssid: get(ssid), | ||||||
|  |         password: get(password), | ||||||
|  |         encryption: get(encryption), | ||||||
|  |         eapMethod: get(eapMethod), | ||||||
|  |         isHiddenSSID: get(isHiddenSSID), | ||||||
|  |         eapAnonymous: get(eapAnonymous), | ||||||
|  |         eapIdentity: get(eapIdentity), | ||||||
|  |         eapPhase2Method: get(eapPhase2Method), | ||||||
|  |       }); | ||||||
|  |       if (text) { | ||||||
|  |         qrcode.value = await QRCode.toDataURL(get(text).trim(), { | ||||||
|  |           color: { | ||||||
|  |             dark: get(foreground), | ||||||
|  |             light: get(background), | ||||||
|  |             ...options?.color, | ||||||
|  |           }, | ||||||
|  |           errorCorrectionLevel: 'M', | ||||||
|  |           ...options, | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { immediate: true }, | ||||||
|  |   ); | ||||||
|  |   return { qrcode, encryption }; | ||||||
|  | } | ||||||
							
								
								
									
										153
									
								
								src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,153 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { | ||||||
|  |   EAPMethods, | ||||||
|  |   EAPPhase2Methods, | ||||||
|  |   useWifiQRCode, | ||||||
|  | } from './useQRCode'; | ||||||
|  | import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||||||
|  | 
 | ||||||
|  | const foreground = ref('#000000ff'); | ||||||
|  | const background = ref('#ffffffff'); | ||||||
|  | 
 | ||||||
|  | const ssid = ref(); | ||||||
|  | const password = ref(); | ||||||
|  | const eapMethod = ref(); | ||||||
|  | const isHiddenSSID = ref(false); | ||||||
|  | const eapAnonymous = ref(false); | ||||||
|  | const eapIdentity = ref(); | ||||||
|  | const eapPhase2Method = ref(); | ||||||
|  | 
 | ||||||
|  | const { qrcode, encryption } = useWifiQRCode({ | ||||||
|  |   ssid, | ||||||
|  |   password, | ||||||
|  |   eapMethod, | ||||||
|  |   isHiddenSSID, | ||||||
|  |   eapAnonymous, | ||||||
|  |   eapIdentity, | ||||||
|  |   eapPhase2Method, | ||||||
|  |   color: { | ||||||
|  |     background, | ||||||
|  |     foreground, | ||||||
|  |   }, | ||||||
|  |   options: { width: 1024 }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-code.png' }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <c-card> | ||||||
|  |     <div grid grid-cols-1 gap-12> | ||||||
|  |       <div> | ||||||
|  |         <c-select | ||||||
|  |           v-model:value="encryption" | ||||||
|  |           mb-4 | ||||||
|  |           label="Encryption method" | ||||||
|  |           default-value="WPA" | ||||||
|  |           label-position="left" | ||||||
|  |           label-width="130px" | ||||||
|  |           label-align="right" | ||||||
|  |           :options="[ | ||||||
|  |             { | ||||||
|  |               label: 'No password', | ||||||
|  |               value: 'nopass', | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               label: 'WPA/WPA2', | ||||||
|  |               value: 'WPA', | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               label: 'WEP', | ||||||
|  |               value: 'WEP', | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               label: 'WPA2-EAP', | ||||||
|  |               value: 'WPA2-EAP', | ||||||
|  |             }, | ||||||
|  |           ]" | ||||||
|  |         /> | ||||||
|  |         <div class="mb-6 flex flex-row items-center gap-2"> | ||||||
|  |           <c-input-text | ||||||
|  |             v-model:value="ssid" | ||||||
|  |             label-position="left" | ||||||
|  |             label-width="130px" | ||||||
|  |             label-align="right" | ||||||
|  |             label="SSID:" | ||||||
|  |             rows="1" | ||||||
|  |             autosize | ||||||
|  |             placeholder="Your WiFi SSID..." | ||||||
|  |             mb-6 | ||||||
|  |           /> | ||||||
|  |           <n-checkbox v-model:checked="isHiddenSSID"> | ||||||
|  |             Hidden SSID | ||||||
|  |           </n-checkbox> | ||||||
|  |         </div> | ||||||
|  |         <c-input-text | ||||||
|  |           v-if="encryption !== 'nopass'" | ||||||
|  |           v-model:value="password" | ||||||
|  |           label-position="left" | ||||||
|  |           label-width="130px" | ||||||
|  |           label-align="right" | ||||||
|  |           label="Password:" | ||||||
|  |           rows="1" | ||||||
|  |           autosize | ||||||
|  |           type="password" | ||||||
|  |           placeholder="Your WiFi Password..." | ||||||
|  |           mb-6 | ||||||
|  |         /> | ||||||
|  |         <c-select | ||||||
|  |           v-if="encryption === 'WPA2-EAP'" | ||||||
|  |           v-model:value="eapMethod" | ||||||
|  |           label="EAP method" | ||||||
|  |           label-position="left" | ||||||
|  |           label-width="130px" | ||||||
|  |           label-align="right" | ||||||
|  |           :options="EAPMethods.map((method) => ({ label: method, value: method }))" | ||||||
|  |           searchable mb-4 | ||||||
|  |         /> | ||||||
|  |         <div v-if="encryption === 'WPA2-EAP'" class="mb-6 flex flex-row items-center gap-2"> | ||||||
|  |           <c-input-text | ||||||
|  |             v-model:value="eapIdentity" | ||||||
|  |             label-position="left" | ||||||
|  |             label-width="130px" | ||||||
|  |             label-align="right" | ||||||
|  |             label="Identity:" | ||||||
|  |             rows="1" | ||||||
|  |             autosize | ||||||
|  |             placeholder="Your EAP Identity..." | ||||||
|  |             mb-6 | ||||||
|  |           /> | ||||||
|  |           <n-checkbox v-model:checked="eapAnonymous"> | ||||||
|  |             Anonymous? | ||||||
|  |           </n-checkbox> | ||||||
|  |         </div> | ||||||
|  |         <c-select | ||||||
|  |           v-if="encryption === 'WPA2-EAP'" | ||||||
|  |           v-model:value="eapPhase2Method" | ||||||
|  |           label="EAP Phase 2 method" | ||||||
|  |           label-position="left" | ||||||
|  |           label-width="130px" | ||||||
|  |           label-align="right" | ||||||
|  |           :options="EAPPhase2Methods.map((method) => ({ label: method, value: method }))" | ||||||
|  |           searchable mb-4 | ||||||
|  |         /> | ||||||
|  |         <n-form label-width="130" label-placement="left"> | ||||||
|  |           <n-form-item label="Foreground color:"> | ||||||
|  |             <n-color-picker v-model:value="foreground" :modes="['hex']" /> | ||||||
|  |           </n-form-item> | ||||||
|  |           <n-form-item label="Background color:"> | ||||||
|  |             <n-color-picker v-model:value="background" :modes="['hex']" /> | ||||||
|  |           </n-form-item> | ||||||
|  |         </n-form> | ||||||
|  |       </div> | ||||||
|  |       <div v-if="qrcode"> | ||||||
|  |         <div flex flex-col items-center gap-3> | ||||||
|  |           <img alt="wifi-qrcode" :src="qrcode" width="200"> | ||||||
|  |           <c-button @click="download"> | ||||||
|  |             Download qr-code | ||||||
|  |           </c-button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
							
								
								
									
										14
									
								
								src/ui/c-buttons-select/c-buttons-select.demo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/ui/c-buttons-select/c-buttons-select.demo.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | const optionsA = [ | ||||||
|  |   { label: 'Option A', value: 'a' }, | ||||||
|  |   { label: 'Option B', value: 'b', tooltip: 'This is a tooltip' }, | ||||||
|  |   { label: 'Option C', value: 'c' }, | ||||||
|  | ]; | ||||||
|  | const valueA = ref('a'); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <c-buttons-select v-model:value="valueA" :options="optionsA" label="Label: " /> | ||||||
|  |   <c-buttons-select v-model:value="valueA" :options="optionsA" label="Label: " label-position="left" mt-2 /> | ||||||
|  |   <c-buttons-select v-model:value="valueA" :options="optionsA" label="Label: " label-position="left" mt-2 /> | ||||||
|  | </template> | ||||||
							
								
								
									
										5
									
								
								src/ui/c-buttons-select/c-buttons-select.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/ui/c-buttons-select/c-buttons-select.types.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | import type { CSelectOption } from '../c-select/c-select.types'; | ||||||
|  | 
 | ||||||
|  | export type CButtonSelectOption<T> = CSelectOption<T> & { | ||||||
|  |   tooltip?: string | ||||||
|  | }; | ||||||
							
								
								
									
										59
									
								
								src/ui/c-buttons-select/c-buttons-select.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/ui/c-buttons-select/c-buttons-select.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | <script setup lang="ts" generic="T extends unknown"> | ||||||
|  | import type { CLabelProps } from '../c-label/c-label.types'; | ||||||
|  | import type { CButtonSelectOption } from './c-buttons-select.types'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults( | ||||||
|  |   defineProps<{ | ||||||
|  |     options?: CButtonSelectOption<T>[] | string[] | ||||||
|  |     value?: T | ||||||
|  |     size?: 'small' | 'medium' | 'large' | ||||||
|  |   } & CLabelProps >(), | ||||||
|  |   { | ||||||
|  |     options: () => [], | ||||||
|  |     value: undefined, | ||||||
|  |     labelPosition: 'left', | ||||||
|  |     size: 'medium', | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const emits = defineEmits(['update:value']); | ||||||
|  | 
 | ||||||
|  | const { options: rawOptions, size } = toRefs(props); | ||||||
|  | 
 | ||||||
|  | const options = computed(() => { | ||||||
|  |   return rawOptions.value.map((option: string | CButtonSelectOption<T>) => { | ||||||
|  |     if (typeof option === 'string') { | ||||||
|  |       return { label: option, value: option }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return option; | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const value = useVModel(props, 'value', emits); | ||||||
|  | 
 | ||||||
|  | function selectOption(option: CButtonSelectOption<T>) { | ||||||
|  |   // @ts-expect-error vue template generic is a bit flacky thanks to withDefaults | ||||||
|  |   value.value = option.value; | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <c-label v-bind="props"> | ||||||
|  |     <div class="flex gap-2"> | ||||||
|  |       <c-tooltip | ||||||
|  |         v-for="option in options" :key="option.value" | ||||||
|  |         :tooltip="option.tooltip" | ||||||
|  |       > | ||||||
|  |         <c-button | ||||||
|  |           :test-id="option.value" | ||||||
|  |           :size="size" | ||||||
|  |           :type="option.value === value ? 'primary' : 'default'" | ||||||
|  |           @click="selectOption(option)" | ||||||
|  |         > | ||||||
|  |           {{ option.label }} | ||||||
|  |         </c-button> | ||||||
|  |       </c-tooltip> | ||||||
|  |     </div> | ||||||
|  |   </c-label> | ||||||
|  | </template> | ||||||
							
								
								
									
										68
									
								
								src/ui/c-diff-editor/c-diff-editor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/ui/c-diff-editor/c-diff-editor.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import * as monaco from 'monaco-editor'; | ||||||
|  | import { useStyleStore } from '@/stores/style.store'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ options?: monaco.editor.IDiffEditorOptions }>(), { options: () => ({}) }); | ||||||
|  | const { options } = toRefs(props); | ||||||
|  | 
 | ||||||
|  | const editorContainer = ref<HTMLElement | null>(null); | ||||||
|  | let editor: monaco.editor.IStandaloneDiffEditor | null = null; | ||||||
|  | 
 | ||||||
|  | monaco.editor.defineTheme('it-tools-dark', { | ||||||
|  |   base: 'vs-dark', | ||||||
|  |   inherit: true, | ||||||
|  |   rules: [], | ||||||
|  |   colors: { | ||||||
|  |     'editor.background': '#00000000', | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | monaco.editor.defineTheme('it-tools-light', { | ||||||
|  |   base: 'vs', | ||||||
|  |   inherit: true, | ||||||
|  |   rules: [], | ||||||
|  |   colors: { | ||||||
|  |     'editor.background': '#00000000', | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const styleStore = useStyleStore(); | ||||||
|  | 
 | ||||||
|  | watch( | ||||||
|  |   () => styleStore.isDarkTheme, | ||||||
|  |   isDarkTheme => monaco.editor.setTheme(isDarkTheme ? 'it-tools-dark' : 'it-tools-light'), | ||||||
|  |   { immediate: true }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | watch( | ||||||
|  |   () => options.value, | ||||||
|  |   options => editor?.updateOptions(options), | ||||||
|  |   { immediate: true, deep: true }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | useResizeObserver(editorContainer, () => { | ||||||
|  |   editor?.layout(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  |   if (!editorContainer.value) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   editor = monaco.editor.createDiffEditor(editorContainer.value, { | ||||||
|  |     originalEditable: true, | ||||||
|  |     minimap: { | ||||||
|  |       enabled: false, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   editor.setModel({ | ||||||
|  |     original: monaco.editor.createModel('original text', 'txt'), | ||||||
|  |     modified: monaco.editor.createModel('modified text', 'txt'), | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div ref="editorContainer" h-600px /> | ||||||
|  | </template> | ||||||
							
								
								
									
										27
									
								
								src/ui/c-key-value-list/c-key-value-list-item.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/ui/c-key-value-list/c-key-value-list-item.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | <script lang="ts" setup> | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import type { CKeyValueListItem } from './c-key-value-list.types'; | ||||||
|  | 
 | ||||||
|  | const props = defineProps<{ item: CKeyValueListItem }>(); | ||||||
|  | const { item } = toRefs(props); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div v-if="_.isArray(item.value)"> | ||||||
|  |     <div v-for="value in item.value" :key="value"> | ||||||
|  |       <c-text-copyable :value="value" :show-icon="item.showCopyButton ?? true" /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |   <div v-else-if="_.isBoolean(item.value)"> | ||||||
|  |     <c-text-copyable :value="item.value ? 'true' : 'false'" :displayed-value="item.value ? 'Yes' : 'No'" :show-icon="item.showCopyButton ?? true" /> | ||||||
|  |   </div> | ||||||
|  |   <div v-else-if="_.isNumber(item.value)" font-mono> | ||||||
|  |     <c-text-copyable :value="String(item.value)" :show-icon="item.showCopyButton ?? true" /> | ||||||
|  |   </div> | ||||||
|  |   <div v-else-if="_.isNil(item.value) || item.value === ''" op-70> | ||||||
|  |     {{ item.placeholder ?? 'N/A' }} | ||||||
|  |   </div> | ||||||
|  |   <div v-else> | ||||||
|  |     <c-text-copyable :value="item.value" :show-icon="item.showCopyButton ?? true" /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
							
								
								
									
										9
									
								
								src/ui/c-key-value-list/c-key-value-list.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/ui/c-key-value-list/c-key-value-list.types.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | export interface CKeyValueListItem { | ||||||
|  |   label: string | ||||||
|  |   value: string | string[] | number | boolean | undefined | null | ||||||
|  |   hideOnNil?: boolean | ||||||
|  |   placeholder?: string | ||||||
|  |   showCopyButton?: boolean | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type CKeyValueListItems = CKeyValueListItem[]; | ||||||
							
								
								
									
										21
									
								
								src/ui/c-key-value-list/c-key-value-list.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/ui/c-key-value-list/c-key-value-list.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | <script lang="ts" setup> | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import type { CKeyValueListItems } from './c-key-value-list.types'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ items?: CKeyValueListItems }>(), { items: () => [] }); | ||||||
|  | const { items } = toRefs(props); | ||||||
|  | 
 | ||||||
|  | const formattedItems = computed(() => items.value.filter(item => !_.isNil(item.value) || !item.hideOnNil)); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div my-5> | ||||||
|  |     <div v-for="item in formattedItems" :key="item.label" flex gap-2 py-1 class="c-key-value-list__item"> | ||||||
|  |       <div flex-basis-180px text-right font-bold class="c-key-value-list__key"> | ||||||
|  |         {{ item.label }} | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <c-key-value-list-item :item="item" class="c-key-value-list__value" /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
| @ -1,11 +1,17 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useTheme } from './c-modal.theme'; | import { useTheme } from './c-modal.theme'; | ||||||
| 
 | 
 | ||||||
|  | defineOptions({ | ||||||
|  |   inheritAttrs: false, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| const props = withDefaults(defineProps<{ open?: boolean; centered?: boolean }>(), { | const props = withDefaults(defineProps<{ open?: boolean; centered?: boolean }>(), { | ||||||
|   open: false, |   open: false, | ||||||
|   centered: true, |   centered: true, | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
| const emit = defineEmits(['update:open']); | const emit = defineEmits(['update:open']); | ||||||
|  | 
 | ||||||
| const isOpen = useVModel(props, 'open', emit, { passive: true }); | const isOpen = useVModel(props, 'open', emit, { passive: true }); | ||||||
| 
 | 
 | ||||||
| const { centered } = toRefs(props); | const { centered } = toRefs(props); | ||||||
| @ -29,10 +35,6 @@ defineExpose({ | |||||||
|   isOpen, |   isOpen, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineOptions({ |  | ||||||
|   inheritAttrs: false, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const theme = useTheme(); | const theme = useTheme(); | ||||||
| const modal = ref(); | const modal = ref(); | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								src/ui/c-text-copyable/c-text-copyable.demo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/ui/c-text-copyable/c-text-copyable.demo.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | <template> | ||||||
|  |   <c-text-copyable value="value" displayed-value="displayedValue" /> | ||||||
|  | </template> | ||||||
							
								
								
									
										17
									
								
								src/ui/c-text-copyable/c-text-copyable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/ui/c-text-copyable/c-text-copyable.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ value?: string; displayedValue?: string; showIcon?: boolean }>(), { value: '', displayedValue: undefined, showIcon: true }); | ||||||
|  | const { value, displayedValue, showIcon } = toRefs(props); | ||||||
|  | 
 | ||||||
|  | const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <c-tooltip :tooltip="isJustCopied ? 'Copied!' : 'Copy to clipboard'" cursor-pointer @click="copy"> | ||||||
|  |     <span flex items-center gap-2> | ||||||
|  |       {{ displayedValue ?? value }} | ||||||
|  |       <icon-mdi-content-copy v-if="showIcon" op-40 /> | ||||||
|  |     </span> | ||||||
|  |   </c-tooltip> | ||||||
|  | </template> | ||||||
							
								
								
									
										17
									
								
								src/ui/c-tooltip/c-tooltip.demo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/ui/c-tooltip/c-tooltip.demo.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-tooltip> | ||||||
|  |       Hover me | ||||||
|  | 
 | ||||||
|  |       <template #tooltip> | ||||||
|  |         Tooltip content | ||||||
|  |       </template> | ||||||
|  |     </c-tooltip> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  |   <div mt-5> | ||||||
|  |     <c-tooltip tooltip="Tooltip content"> | ||||||
|  |       Hover me | ||||||
|  |     </c-tooltip> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
							
								
								
									
										31
									
								
								src/ui/c-tooltip/c-tooltip.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/ui/c-tooltip/c-tooltip.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | const props = withDefaults(defineProps<{ tooltip?: string }>(), { tooltip: '' }); | ||||||
|  | const { tooltip } = toRefs(props); | ||||||
|  | 
 | ||||||
|  | const targetRef = ref(); | ||||||
|  | const isTargetHovered = useElementHover(targetRef); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div class="relative" inline-block> | ||||||
|  |     <div ref="targetRef"> | ||||||
|  |       <slot /> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div | ||||||
|  |       v-if="tooltip || $slots.tooltip" | ||||||
|  |       class="absolute bottom-100% left-50% z-10 mb-5px whitespace-nowrap rounded bg-black px-12px py-6px text-sm text-white shadow-lg transition transition transition-duration-0.2s -translate-x-1/2" | ||||||
|  |       :class="{ | ||||||
|  |         'op-0 scale-0': isTargetHovered === false, | ||||||
|  |         'op-100 scale-100': isTargetHovered, | ||||||
|  |       }" | ||||||
|  |     > | ||||||
|  |       <slot | ||||||
|  |         v-if="isTargetHovered" | ||||||
|  |         name="tooltip" | ||||||
|  |       > | ||||||
|  |         {{ tooltip }} | ||||||
|  |       </slot> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
| @ -4,7 +4,7 @@ const clampHex = (value: number) => Math.max(0, Math.min(255, Math.round(value)) | |||||||
| 
 | 
 | ||||||
| function lighten(color: string, amount: number): string { | function lighten(color: string, amount: number): string { | ||||||
|   const alpha = color.length === 9 ? color.slice(7) : ''; |   const alpha = color.length === 9 ? color.slice(7) : ''; | ||||||
|   const num = parseInt(color.slice(1, 7), 16); |   const num = Number.parseInt(color.slice(1, 7), 16); | ||||||
| 
 | 
 | ||||||
|   const r = clampHex(((num >> 16) & 255) + amount); |   const r = clampHex(((num >> 16) & 255) + amount); | ||||||
|   const g = clampHex(((num >> 8) & 255) + amount); |   const g = clampHex(((num >> 8) & 255) + amount); | ||||||
|  | |||||||
| @ -7,5 +7,5 @@ export function formatBytes(bytes: number, decimals = 2) { | |||||||
|   const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; |   const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; | ||||||
|   const i = Math.floor(Math.log(bytes) / Math.log(k)); |   const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||||||
| 
 | 
 | ||||||
|   return `${parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`; |   return `${Number.parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,14 +1,20 @@ | |||||||
| { | { | ||||||
|   "extends": "@vue/tsconfig/tsconfig.web.json", |   "extends": "@vue/tsconfig/tsconfig.json", | ||||||
|   "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "**/*.d.ts", "node_modules/vite-plugin-pwa/client.d.ts"], |   "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "**/*.d.ts", "node_modules/vite-plugin-pwa/client.d.ts"], | ||||||
|   "exclude": ["src/**/__tests__/*"], |   "exclude": ["src/**/__tests__/*"], | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "lib": ["ES2021"], |     "lib": ["ES2022"], | ||||||
|  |     "target": "es2022", | ||||||
|  |     "module": "es2022", | ||||||
|  |     "moduleResolution": "Node", | ||||||
|     "composite": true, |     "composite": true, | ||||||
|     "baseUrl": ".", |     "baseUrl": ".", | ||||||
|     "paths": { |     "paths": { | ||||||
|       "@/*": ["./src/*"] |       "@/*": ["./src/*"] | ||||||
|     }, |     }, | ||||||
|     "types": ["naive-ui/volar", "unplugin-icons/types/vue", "@intlify/unplugin-vue-i18n/messages"] |     "types": ["naive-ui/volar", "@intlify/unplugin-vue-i18n/messages", "unplugin-icons/types/vue"], | ||||||
|  |     "esModuleInterop": true, | ||||||
|  |     "jsx": "preserve", | ||||||
|  |     "skipLibCheck": true | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| { | { | ||||||
|   "extends": "@vue/tsconfig/tsconfig.node.json", |   "extends": "@tsconfig/node18/tsconfig.json", | ||||||
|   "include": ["vite.config.*"], |   "include": ["vite.config.*"], | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "composite": true, |     "composite": true, | ||||||
|  | |||||||
| @ -4,6 +4,6 @@ | |||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "composite": true, |     "composite": true, | ||||||
|     "lib": [], |     "lib": [], | ||||||
|     "types": ["node", "jsdom"] |     "types": ["node", "jsdom", "unplugin-icons/types/vue"] | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ export default defineConfig({ | |||||||
|       runtimeOnly: true, |       runtimeOnly: true, | ||||||
|       compositionOnly: true, |       compositionOnly: true, | ||||||
|       fullInstall: true, |       fullInstall: true, | ||||||
|       include: [resolve(__dirname, 'locales/**'), resolve(__dirname, 'src/tools/*/locales/**')], |       include: [resolve(__dirname, 'locales/**')], | ||||||
|     }), |     }), | ||||||
|     AutoImport({ |     AutoImport({ | ||||||
|       imports: [ |       imports: [ | ||||||
| @ -106,4 +106,7 @@ export default defineConfig({ | |||||||
|   test: { |   test: { | ||||||
|     exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'], |     exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'], | ||||||
|   }, |   }, | ||||||
|  |   build: { | ||||||
|  |     target: 'esnext', | ||||||
|  |   }, | ||||||
| }); | }); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user