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/no-use-before-define': ['error', { allowNamedExports: true, functions: false }], | ||||
|     '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 | ||||
| 
 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||
|       - run: corepack enable | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @ -37,7 +37,7 @@ jobs: | ||||
| 
 | ||||
|     steps: | ||||
|     - name: Checkout repository | ||||
|       uses: actions/checkout@v3 | ||||
|       uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||
| 
 | ||||
|     # Initializes the CodeQL tools for scanning. | ||||
|     - 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: | ||||
|       should_run: ${{ steps.should_run.outputs.should_run }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||
|       - name: print latest_commit | ||||
|         run: echo ${{ github.sha }} | ||||
| 
 | ||||
| @ -28,7 +28,7 @@ jobs: | ||||
|     if: ${{ needs.check_date.outputs.should_run != 'false' }} | ||||
| 
 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||
|       - run: corepack enable | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
| @ -54,7 +54,7 @@ jobs: | ||||
| 
 | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||
| 
 | ||||
|       - name: Login to GitHub Container Registry | ||||
|         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 | ||||
| on: [deployment_status] | ||||
| 
 | ||||
| on: | ||||
|   pull_request: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
| jobs: | ||||
|   test: | ||||
|     if: github.event.deployment_status.state == 'success' | ||||
|     timeout-minutes: 60 | ||||
|     runs-on: ubuntu-latest | ||||
|     env: | ||||
|       BASE_URL: ${{ github.event.deployment_status.target_url }} | ||||
|     strategy: | ||||
|       matrix: | ||||
|         shard: [1/3, 2/3, 3/3] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||
| 
 | ||||
|       - run: corepack enable | ||||
| 
 | ||||
| @ -28,6 +28,9 @@ jobs: | ||||
|       - name: Install dependencies | ||||
|         run: pnpm install | ||||
| 
 | ||||
|       - name: Build app | ||||
|         run: pnpm build | ||||
| 
 | ||||
|       - name: Restore Playwright browsers from cache | ||||
|         uses: actions/cache@v3 | ||||
|         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 | ||||
| 
 | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||
| 
 | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v2 | ||||
| @ -55,7 +55,7 @@ jobs: | ||||
|         run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV | ||||
| 
 | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 | ||||
| 
 | ||||
|       - 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. | ||||
| 
 | ||||
| ## 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 | ||||
| 
 | ||||
| ### Features | ||||
|  | ||||
							
								
								
									
										17
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								README.md
									
									
									
									
									
								
							| @ -26,6 +26,7 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent | ||||
| 
 | ||||
| **Other solutions:** | ||||
| 
 | ||||
| - [Cloudron](https://www.cloudron.io/store/tech.ittools.cloudron.html) | ||||
| - [Tipi](https://www.runtipi.io/docs/apps-available) | ||||
| - [Unraid](https://unraid.net/community/apps?q=it-tools) | ||||
| 
 | ||||
| @ -34,6 +35,7 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent | ||||
| ### Recommended IDE Setup | ||||
| 
 | ||||
| [VSCode](https://code.visualstudio.com/) with the following extensions: | ||||
| 
 | ||||
| - [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) | ||||
| - [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). | ||||
| - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) | ||||
| @ -41,16 +43,13 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent | ||||
| 
 | ||||
| with the following settings: | ||||
| 
 | ||||
| ```json5 | ||||
| ```json | ||||
| { | ||||
|   "editor.formatOnSave": false, | ||||
|   "editor.codeActionsOnSave": { | ||||
|     "source.fixAll.eslint": true | ||||
|   }, | ||||
|   "i18n-ally.localesPaths": [ | ||||
|     "locales", | ||||
|     "src/tools/*/locales" | ||||
|   ], | ||||
|   "i18n-ally.localesPaths": ["locales", "src/tools/*/locales"], | ||||
|   "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. | ||||
| 
 | ||||
| ## Contributors | ||||
| 
 | ||||
| Big thanks to all the people who have already contributed! | ||||
| 
 | ||||
| [](https://github.com/corentinth/it-tools/graphs/contributors) | ||||
| 
 | ||||
| ## Credits | ||||
| 
 | ||||
| Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr). | ||||
| 
 | ||||
| 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-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'] | ||||
|     CButton: typeof import('./src/ui/c-button/c-button.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.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default'] | ||||
|     CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default'] | ||||
|     ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default'] | ||||
|     Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] | ||||
|     CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default'] | ||||
|     'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] | ||||
|     CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default'] | ||||
|     CKeyValueListItem: typeof import('./src/ui/c-key-value-list/c-key-value-list-item.vue')['default'] | ||||
|     CLabel: typeof import('./src/ui/c-label/c-label.vue')['default'] | ||||
|     CLink: typeof import('./src/ui/c-link/c-link.vue')['default'] | ||||
|     'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] | ||||
| @ -44,8 +49,11 @@ declare module '@vue/runtime-core' { | ||||
|     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] | ||||
|     CSelect: typeof import('./src/ui/c-select/c-select.vue')['default'] | ||||
|     'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default'] | ||||
|     CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default'] | ||||
|     'CTextCopyable.demo': typeof import('./src/ui/c-text-copyable/c-text-copyable.demo.vue')['default'] | ||||
|     CTooltip: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default'] | ||||
|     'CTooltip.demo': typeof import('./src/ui/c-tooltip/c-tooltip.demo.vue')['default'] | ||||
|     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] | ||||
|     'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default'] | ||||
|     'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default'] | ||||
|     DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] | ||||
|     DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default'] | ||||
| @ -68,12 +76,12 @@ declare module '@vue/runtime-core' { | ||||
|     HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default'] | ||||
|     HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default'] | ||||
|     HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] | ||||
|     IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default'] | ||||
|     'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] | ||||
|     'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default'] | ||||
|     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] | ||||
|     IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] | ||||
|     IconMdiCamera: typeof import('~icons/mdi/camera')['default'] | ||||
|     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'] | ||||
|     IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] | ||||
|     IconMdiClose: typeof import('~icons/mdi/close')['default'] | ||||
| @ -82,14 +90,11 @@ declare module '@vue/runtime-core' { | ||||
|     IconMdiDownload: typeof import('~icons/mdi/download')['default'] | ||||
|     IconMdiEye: typeof import('~icons/mdi/eye')['default'] | ||||
|     IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] | ||||
|     IconMdiMagnify: typeof import('~icons/mdi/magnify')['default'] | ||||
|     IconMdiPause: typeof import('~icons/mdi/pause')['default'] | ||||
|     IconMdiPlay: typeof import('~icons/mdi/play')['default'] | ||||
|     IconMdiRecord: typeof import('~icons/mdi/record')['default'] | ||||
|     IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] | ||||
|     IconMdiSearch: typeof import('~icons/mdi/search')['default'] | ||||
|     IconMdiSearchRound: typeof import('~icons/mdi/search-round')['default'] | ||||
|     IconMdiTea: typeof import('~icons/mdi/tea')['default'] | ||||
|     IconMdiVideo: typeof import('~icons/mdi/video')['default'] | ||||
|     InputCopyable: typeof import('./src/components/InputCopyable.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'] | ||||
|     NIcon: typeof import('naive-ui')['NIcon'] | ||||
|     NImage: typeof import('naive-ui')['NImage'] | ||||
|     NInput: typeof import('naive-ui')['NInput'] | ||||
|     NInputGroup: typeof import('naive-ui')['NInputGroup'] | ||||
|     NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] | ||||
|     NInputNumber: typeof import('naive-ui')['NInputNumber'] | ||||
| @ -146,13 +150,11 @@ declare module '@vue/runtime-core' { | ||||
|     NPageHeader: typeof import('naive-ui')['NPageHeader'] | ||||
|     NProgress: typeof import('naive-ui')['NProgress'] | ||||
|     NScrollbar: typeof import('naive-ui')['NScrollbar'] | ||||
|     NSelect: typeof import('naive-ui')['NSelect'] | ||||
|     NSlider: typeof import('naive-ui')['NSlider'] | ||||
|     NStatistic: typeof import('naive-ui')['NStatistic'] | ||||
|     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||
|     NTable: typeof import('naive-ui')['NTable'] | ||||
|     NTag: typeof import('naive-ui')['NTag'] | ||||
|     NText: typeof import('naive-ui')['NText'] | ||||
|     NTooltip: typeof import('naive-ui')['NTooltip'] | ||||
|     NUpload: typeof import('naive-ui')['NUpload'] | ||||
|     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'] | ||||
|     SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] | ||||
|     SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default'] | ||||
|     StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default'] | ||||
|     SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default'] | ||||
|     TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default'] | ||||
|     TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default'] | ||||
|     TextDiff: typeof import('./src/tools/text-diff/text-diff.vue')['default'] | ||||
|     TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default'] | ||||
|     TextToNatoAlphabet: typeof import('./src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue')['default'] | ||||
|     TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default'] | ||||
| @ -181,11 +185,13 @@ declare module '@vue/runtime-core' { | ||||
|     TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.vue')['default'] | ||||
|     'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default'] | ||||
|     ToolCard: typeof import('./src/components/ToolCard.vue')['default'] | ||||
|     UlidGenerator: typeof import('./src/tools/ulid-generator/ulid-generator.vue')['default'] | ||||
|     UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default'] | ||||
|     UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default'] | ||||
|     UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default'] | ||||
|     UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default'] | ||||
|     UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default'] | ||||
|     WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default'] | ||||
|     XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default'] | ||||
|     YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default'] | ||||
|     YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default'] | ||||
|  | ||||
							
								
								
									
										44
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								package.json
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "it-tools", | ||||
|   "version": "2023.5.14-77f2efc", | ||||
|   "version": "2023.8.21-6f93cba", | ||||
|   "description": "Collection of handy online tools for developers, with great UX. ", | ||||
|   "keywords": [ | ||||
|     "productivity", | ||||
| @ -21,11 +21,12 @@ | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "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", | ||||
|     "test": "npm run test:unit", | ||||
|     "test:unit": "vitest --environment jsdom", | ||||
|     "test:e2e": "playwright test", | ||||
|     "test:e2e:dev": "BASE_URL=http://localhost:5173 NO_WEB_SERVER=true playwright test", | ||||
|     "coverage": "vitest run --coverage", | ||||
|     "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", | ||||
| @ -36,12 +37,12 @@ | ||||
|     "@it-tools/bip39": "^0.0.4", | ||||
|     "@it-tools/oggen": "^1.3.0", | ||||
|     "@sindresorhus/slugify": "^2.2.0", | ||||
|     "@tiptap/pm": "^2.0.3", | ||||
|     "@tiptap/starter-kit": "^2.0.3", | ||||
|     "@tiptap/pm": "^2.1.6", | ||||
|     "@tiptap/starter-kit": "^2.1.6", | ||||
|     "@tiptap/vue-3": "^2.0.3", | ||||
|     "@vicons/material": "^0.12.0", | ||||
|     "@vicons/tabler": "^0.12.0", | ||||
|     "@vueuse/core": "^10.1.2", | ||||
|     "@vueuse/core": "^10.3.0", | ||||
|     "@vueuse/head": "^1.0.0", | ||||
|     "@vueuse/router": "^10.0.0", | ||||
|     "bcryptjs": "^2.4.3", | ||||
| @ -58,12 +59,14 @@ | ||||
|     "fuse.js": "^6.6.2", | ||||
|     "highlight.js": "^11.7.0", | ||||
|     "iarna-toml-esm": "^3.0.5", | ||||
|     "ibantools": "^4.3.3", | ||||
|     "json5": "^2.2.3", | ||||
|     "jwt-decode": "^3.1.2", | ||||
|     "libphonenumber-js": "^1.10.28", | ||||
|     "lodash": "^4.17.21", | ||||
|     "mathjs": "^11.0.0", | ||||
|     "mathjs": "^11.9.1", | ||||
|     "mime-types": "^2.1.35", | ||||
|     "monaco-editor": "^0.43.0", | ||||
|     "naive-ui": "^2.34.3", | ||||
|     "netmask": "^2.0.2", | ||||
|     "node-forge": "^1.3.1", | ||||
| @ -72,9 +75,9 @@ | ||||
|     "plausible-tracker": "^0.3.8", | ||||
|     "qrcode": "^1.5.1", | ||||
|     "randombytes": "^2.1.0", | ||||
|     "sql-formatter": "^8.2.0", | ||||
|     "ts-pattern": "^4.2.2", | ||||
|     "sql-formatter": "^13.0.0", | ||||
|     "ua-parser-js": "^1.0.35", | ||||
|     "ulid": "^2.3.0", | ||||
|     "unicode-emoji-json": "^0.4.0", | ||||
|     "unplugin-auto-import": "^0.16.4", | ||||
|     "uuid": "^9.0.0", | ||||
| @ -86,44 +89,43 @@ | ||||
|     "yaml": "^2.2.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@antfu/eslint-config": "^0.39.3", | ||||
|     "@antfu/eslint-config": "^0.41.0", | ||||
|     "@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", | ||||
|     "@rushstack/eslint-patch": "^1.2.0", | ||||
|     "@tsconfig/node18": "^18.2.0", | ||||
|     "@types/bcryptjs": "^2.4.2", | ||||
|     "@types/crypto-js": "^4.1.1", | ||||
|     "@types/jsdom": "^21.0.0", | ||||
|     "@types/lodash": "^4.14.192", | ||||
|     "@types/mime-types": "^2.1.1", | ||||
|     "@types/netmask": "^2.0.0", | ||||
|     "@types/node": "^18.0.0", | ||||
|     "@types/node": "^18.15.11", | ||||
|     "@types/node-forge": "^1.3.2", | ||||
|     "@types/prettier": "^2.7.2", | ||||
|     "@types/qrcode": "^1.5.0", | ||||
|     "@types/randombytes": "^2.0.0", | ||||
|     "@types/ua-parser-js": "^0.7.36", | ||||
|     "@types/uuid": "^9.0.0", | ||||
|     "@typescript-eslint/parser": "^5.58.0", | ||||
|     "@unocss/eslint-config": "^0.55.0", | ||||
|     "@vitejs/plugin-vue": "^4.0.0", | ||||
|     "@vitejs/plugin-vue-jsx": "^3.0.0", | ||||
|     "@vitejs/plugin-vue": "^4.3.2", | ||||
|     "@vitejs/plugin-vue-jsx": "^3.0.2", | ||||
|     "@vue/compiler-sfc": "^3.2.47", | ||||
|     "@vue/runtime-dom": "^3.3.4", | ||||
|     "@vue/test-utils": "^2.3.2", | ||||
|     "@vue/tsconfig": "^0.1.3", | ||||
|     "@vue/tsconfig": "^0.4.0", | ||||
|     "c8": "^8.0.0", | ||||
|     "consola": "^3.0.2", | ||||
|     "eslint": "^8.38.0", | ||||
|     "eslint": "^8.47.0", | ||||
|     "jsdom": "^22.0.0", | ||||
|     "less": "^4.1.3", | ||||
|     "prettier": "^2.8.7", | ||||
|     "typescript": "~4.9.0", | ||||
|     "prettier": "^3.0.0", | ||||
|     "typescript": "~5.2.0", | ||||
|     "unocss": "^0.55.0", | ||||
|     "unocss-preset-scrollbar": "^0.2.1", | ||||
|     "unplugin-icons": "^0.16.1", | ||||
|     "unplugin-icons": "^0.17.0", | ||||
|     "unplugin-vue-components": "^0.25.0", | ||||
|     "vite": "^4.0.0", | ||||
|     "vite": "^4.4.9", | ||||
|     "vite-plugin-pwa": "^0.16.0", | ||||
|     "vite-plugin-vue-markdown": "^0.23.5", | ||||
|     "vite-svg-loader": "^4.0.0", | ||||
|  | ||||
| @ -2,6 +2,7 @@ import { defineConfig, devices } from '@playwright/test'; | ||||
| 
 | ||||
| const isCI = !!process.env.CI; | ||||
| const baseUrl = process.env.BASE_URL || 'http://localhost:5050'; | ||||
| const useWebServer = process.env.NO_WEB_SERVER !== 'true'; | ||||
| 
 | ||||
| /** | ||||
|  * See https://playwright.dev/docs/test-configuration.
 | ||||
| @ -52,13 +53,13 @@ export default defineConfig({ | ||||
| 
 | ||||
|   /* Run your local dev server before starting the tests */ | ||||
| 
 | ||||
|   ...(isCI | ||||
|     ? {} | ||||
|     : { | ||||
|   ...(useWebServer | ||||
|     && { | ||||
|       webServer: { | ||||
|         command: 'npm run preview', | ||||
|         url: 'http://127.0.0.1:5050', | ||||
|           reuseExistingServer: true, | ||||
|         reuseExistingServer: !isCI, | ||||
|       }, | ||||
|       }), | ||||
|     } | ||||
|   ), | ||||
| }); | ||||
|  | ||||
							
								
								
									
										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"> | ||||
| import { useClipboard, useVModel } from '@vueuse/core'; | ||||
| import { useVModel } from '@vueuse/core'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| 
 | ||||
| const props = defineProps<{ value: string }>(); | ||||
| const emit = defineEmits(['update:value']); | ||||
| 
 | ||||
| const value = useVModel(props, 'value', emit); | ||||
| const tooltipText = ref('Copy to clipboard'); | ||||
| 
 | ||||
| const { copy } = useClipboard({ source: value }); | ||||
| 
 | ||||
| function onCopyClicked() { | ||||
|   copy(); | ||||
|   tooltipText.value = 'Copied!'; | ||||
| 
 | ||||
|   setTimeout(() => { | ||||
|     tooltipText.value = 'Copy to clipboard'; | ||||
|   }, 2000); | ||||
| } | ||||
| const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||
| const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : 'Copy to clipboard'); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
| @ -24,7 +15,7 @@ function onCopyClicked() { | ||||
|     <template #suffix> | ||||
|       <n-tooltip trigger="hover"> | ||||
|         <template #trigger> | ||||
|           <c-button circle variant="text" size="small" @click="onCopyClicked"> | ||||
|           <c-button circle variant="text" size="small" @click="copy()"> | ||||
|             <icon-mdi-content-copy /> | ||||
|           </c-button> | ||||
|         </template> | ||||
|  | ||||
| @ -1,26 +1,19 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useClipboard } from '@vueuse/core'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ value?: string }>(), { value: '' }); | ||||
| const { value } = toRefs(props); | ||||
| 
 | ||||
| const initialText = 'Copy to clipboard'; | ||||
| const tooltipText = ref(initialText); | ||||
| 
 | ||||
| const { copy } = useClipboard({ source: value }); | ||||
| 
 | ||||
| function handleClick() { | ||||
|   copy(); | ||||
|   tooltipText.value = 'Copied!'; | ||||
| 
 | ||||
|   setTimeout(() => (tooltipText.value = initialText), 1000); | ||||
| } | ||||
| const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||
| const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : initialText); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <n-tooltip trigger="hover"> | ||||
|     <template #trigger> | ||||
|       <span class="value" @click="handleClick">{{ value }}</span> | ||||
|       <span class="value" @click="copy()">{{ value }}</span> | ||||
|     </template> | ||||
|     {{ tooltipText }} | ||||
|   </n-tooltip> | ||||
|  | ||||
| @ -1,12 +1,13 @@ | ||||
| <script setup lang="ts"> | ||||
| import { Copy } from '@vicons/tabler'; | ||||
| import { useClipboard, useElementSize } from '@vueuse/core'; | ||||
| import { useElementSize } from '@vueuse/core'; | ||||
| import hljs from 'highlight.js/lib/core'; | ||||
| import jsonHljs from 'highlight.js/lib/languages/json'; | ||||
| import sqlHljs from 'highlight.js/lib/languages/sql'; | ||||
| import xmlHljs from 'highlight.js/lib/languages/xml'; | ||||
| import yamlHljs from 'highlight.js/lib/languages/yaml'; | ||||
| import iniHljs from 'highlight.js/lib/languages/ini'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| 
 | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
| @ -33,17 +34,8 @@ hljs.registerLanguage('toml', iniHljs); | ||||
| const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); | ||||
| const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) }; | ||||
| 
 | ||||
| const { copy } = useClipboard({ source: value }); | ||||
| const tooltipText = ref(copyMessage.value); | ||||
| 
 | ||||
| function onCopyClicked() { | ||||
|   copy(); | ||||
|   tooltipText.value = 'Copied !'; | ||||
| 
 | ||||
|   setTimeout(() => { | ||||
|     tooltipText.value = copyMessage.value; | ||||
|   }, 2000); | ||||
| } | ||||
| const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||
| const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.value); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
| @ -61,7 +53,7 @@ function onCopyClicked() { | ||||
|       <n-tooltip v-if="value" trigger="hover"> | ||||
|         <template #trigger> | ||||
|           <div class="copy-button" :class="[copyPlacement]"> | ||||
|             <c-button circle important:h-10 important:w-10 @click="onCopyClicked"> | ||||
|             <c-button circle important:h-10 important:w-10 @click="copy()"> | ||||
|               <n-icon size="22" :component="Copy" /> | ||||
|             </c-button> | ||||
|           </div> | ||||
| @ -70,7 +62,7 @@ function onCopyClicked() { | ||||
|       </n-tooltip> | ||||
|     </c-card> | ||||
|     <div v-if="copyPlacement === 'outside'" mt-4 flex justify-center> | ||||
|       <c-button @click="onCopyClicked"> | ||||
|       <c-button @click="copy()"> | ||||
|         {{ tooltipText }} | ||||
|       </c-button> | ||||
|     </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 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(); | ||||
| 
 | ||||
|   return { | ||||
|     ...rest, | ||||
|     isJustCopied: copied, | ||||
|     async copy(content?: string, { notificationMessage }: { notificationMessage?: string } = {}) { | ||||
|       if (source) { | ||||
|         await copy(); | ||||
| @ -14,7 +22,9 @@ export function useCopy({ source, text = 'Copied to the clipboard' }: { source?: | ||||
|         await copy(content); | ||||
|       } | ||||
| 
 | ||||
|       if (createToast) { | ||||
|         message.success(notificationMessage ?? text); | ||||
|       } | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| @ -49,7 +49,7 @@ const { t } = useI18n(); | ||||
|       </transition> | ||||
| 
 | ||||
|       <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-gi v-for="tool in toolStore.newTools" :key="tool.name"> | ||||
|             <ToolCard :tool="tool" /> | ||||
|  | ||||
| @ -1,6 +1,22 @@ | ||||
| import type { App } from 'vue'; | ||||
| import type { Plugin } from 'vue'; | ||||
| 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({ | ||||
|   legacy: false, | ||||
| @ -8,8 +24,8 @@ const i18n = createI18n({ | ||||
|   messages, | ||||
| }); | ||||
| 
 | ||||
| export const i18nPlugin = { | ||||
|   install: (app: App) => { | ||||
| export const i18nPlugin: Plugin = { | ||||
|   install: (app) => { | ||||
|     app.use(i18n); | ||||
|   }, | ||||
| }; | ||||
|  | ||||
							
								
								
									
										28
									
								
								src/shims.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								src/shims.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -1,21 +1,35 @@ | ||||
| declare module '*.vue' { | ||||
|   import type { ComponentOptions, ComponentOptions } from 'vue'; | ||||
|   import type {  ComponentOptions } from 'vue'; | ||||
|   const Component: ComponentOptions; | ||||
|   export default Component; | ||||
| } | ||||
| 
 | ||||
| declare module '*.md' { | ||||
|   import type {  ComponentOptions } from 'vue'; | ||||
|   const Component: ComponentOptions; | ||||
|   export default Component; | ||||
| } | ||||
| 
 | ||||
| declare module '~icons/*' { | ||||
|   import { FunctionalComponent, SVGAttributes } from 'vue'; | ||||
|   const component: FunctionalComponent<SVGAttributes>; | ||||
|   export default component; | ||||
| } | ||||
| 
 | ||||
| declare module 'iarna-toml-esm' { | ||||
|   export const parse: (toml: string) => any; | ||||
|   export const stringify: (obj: any) => string; | ||||
| } | ||||
| 
 | ||||
| declare module 'emojilib' { | ||||
|   const lib: Record<string, string[]>; | ||||
|   export default lib; | ||||
| } | ||||
| 
 | ||||
| declare module 'unicode-emoji-json' { | ||||
|   const emoji: Record<string, { | ||||
|     name: string; | ||||
|     slug: string; | ||||
|     group: string; | ||||
|     emoji_version: string; | ||||
|     unicode_version: string; | ||||
|     skin_tone_support: boolean; | ||||
|     skin_tone_support_unicode_version: string; | ||||
|   }>; | ||||
|    | ||||
|   export default emoji; | ||||
| } | ||||
| @ -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 /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Your hash: " label-placement="left"> | ||||
|         <c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text /> | ||||
|         <c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> | ||||
|         <div class="compare-result" :class="{ positive: compareMatch }"> | ||||
|  | ||||
| @ -18,7 +18,7 @@ function computeVariance({ data }: { data: number[] }) { | ||||
|   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) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
| @ -1,10 +1,11 @@ | ||||
| <script setup lang="ts"> | ||||
| import { Plus, Trash } from '@vicons/tabler'; | ||||
| import { useClipboard, useStorage } from '@vueuse/core'; | ||||
| import { useStorage } from '@vueuse/core'; | ||||
| import _ from 'lodash'; | ||||
| 
 | ||||
| import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models'; | ||||
| import DynamicValues from './dynamic-values.vue'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| 
 | ||||
| const suites = useStorage('benchmark-builder:suites', [ | ||||
|   { title: 'Suite 1', data: [5, 10] }, | ||||
| @ -47,7 +48,7 @@ const results = computed(() => { | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| const { copy } = useClipboard(); | ||||
| const { copy } = useCopy({ createToast: false }); | ||||
| 
 | ||||
| const header = { | ||||
|   title: 'Suite', | ||||
|  | ||||
| @ -92,7 +92,7 @@ const inputLabelAlignmentConfig = { | ||||
|       v-bind="inputLabelAlignmentConfig" | ||||
|     /> | ||||
| 
 | ||||
|     <div divider my-16px /> | ||||
|     <div my-16px divider /> | ||||
| 
 | ||||
|     <InputCopyable | ||||
|       v-for="format in formats" | ||||
|  | ||||
| @ -82,7 +82,7 @@ const formats: DateFormat[] = [ | ||||
|   { | ||||
|     name: 'Mongo ObjectID', | ||||
|     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), | ||||
|   }, | ||||
| ]; | ||||
| @ -146,7 +146,7 @@ function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date | ||||
|       <c-input-text | ||||
|         v-model:value="inputDate" | ||||
|         autofocus | ||||
|         placeholder="Put you date string here..." | ||||
|         placeholder="Put your date string here..." | ||||
|         clearable | ||||
|         test-id="date-time-converter-input" | ||||
|         :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> | ||||
|       </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` })"> | ||||
|           {{ emojiInfo.codePoints }} | ||||
|         </span> | ||||
|  | ||||
| @ -5,4 +5,4 @@ export type EmojiInfo = { | ||||
|   emoji: string | ||||
|   codePoints: string | undefined | ||||
|   unicode: string | ||||
| } & typeof emojiUnicodeData['\uD83E\uDD10']; | ||||
| } & typeof emojiUnicodeData[string]; | ||||
|  | ||||
| @ -2,6 +2,6 @@ export function convertHexToBin(hex: string) { | ||||
|   return hex | ||||
|     .trim() | ||||
|     .split('') | ||||
|     .map(byte => parseInt(byte, 16).toString(2).padStart(4, '0')) | ||||
|     .map(byte => Number.parseInt(byte, 16).toString(2).padStart(4, '0')) | ||||
|     .join(''); | ||||
| } | ||||
|  | ||||
| @ -1,14 +1,16 @@ | ||||
| <script setup lang="ts"> | ||||
| import { format } from 'prettier'; | ||||
| import htmlParser from 'prettier/parser-html'; | ||||
| import htmlParser from 'prettier/plugins/html'; | ||||
| import { useStorage } from '@vueuse/core'; | ||||
| import Editor from './editor/editor.vue'; | ||||
| import TextareaCopyable from '@/components/TextareaCopyable.vue'; | ||||
| 
 | ||||
| const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>'); | ||||
| 
 | ||||
| const formattedHtml = asyncComputed(() => format(html.value, { parser: 'html', plugins: [htmlParser] }), ''); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Editor v-model:html="html" /> | ||||
|   <TextareaCopyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" /> | ||||
|   <TextareaCopyable :value="formattedHtml" language="html" /> | ||||
| </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 base64StringConverter } from './base64-string-converter'; | ||||
| 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 passwordStrengthAnalyser } from './password-strength-analyser'; | ||||
| 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 otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator'; | ||||
| 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 romanNumeralConverter } from './roman-numeral-converter'; | ||||
| import { tool as sqlPrettify } from './sql-prettify'; | ||||
| @ -70,7 +75,7 @@ import { tool as xmlFormatter } from './xml-formatter'; | ||||
| export const toolsByCategory: ToolCategory[] = [ | ||||
|   { | ||||
|     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', | ||||
| @ -114,7 +119,7 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|   }, | ||||
|   { | ||||
|     name: 'Images and videos', | ||||
|     components: [qrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], | ||||
|     components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], | ||||
|   }, | ||||
|   { | ||||
|     name: 'Development', | ||||
| @ -145,11 +150,11 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|   }, | ||||
|   { | ||||
|     name: 'Text', | ||||
|     components: [loremIpsumGenerator, textStatistics, emojiPicker], | ||||
|     components: [loremIpsumGenerator, textStatistics, emojiPicker, stringObfuscator, textDiff], | ||||
|   }, | ||||
|   { | ||||
|     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) | ||||
|       .trim() | ||||
|       .split('.') | ||||
|       .map(part => parseInt(part).toString(16).padStart(2, '0')) | ||||
|       .map(part => Number.parseInt(part).toString(16).padStart(2, '0')) | ||||
|       .chunk(2) | ||||
|       .map(blocks => blocks.join('')) | ||||
|       .join(':') | ||||
|  | ||||
| @ -13,7 +13,7 @@ function getRangesize(start: string, end: string) { | ||||
|     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) { | ||||
| @ -55,8 +55,8 @@ function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) { | ||||
|   const cidr = getCidr(start, end); | ||||
|   if (cidr != null) { | ||||
|     const result: Ipv4RangeExpanderResult = {}; | ||||
|     result.newEnd = bits2ip(parseInt(cidr.end, 2)); | ||||
|     result.newStart = bits2ip(parseInt(cidr.start, 2)); | ||||
|     result.newEnd = bits2ip(Number.parseInt(cidr.end, 2)); | ||||
|     result.newStart = bits2ip(Number.parseInt(cidr.start, 2)); | ||||
|     result.newCidr = `${result.newStart}/${cidr.mask}`; | ||||
|     result.newSize = getRangesize(cidr.start, cidr.end); | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| import jwtDecode, { type JwtHeader, type JwtPayload } from 'jwt-decode'; | ||||
| import _ from 'lodash'; | ||||
| import { match } from 'ts-pattern'; | ||||
| import { ALGORITHM_DESCRIPTIONS, CLAIM_DESCRIPTIONS } from './jwt-parser.constants'; | ||||
| 
 | ||||
| export { decodeJwt }; | ||||
| @ -32,10 +31,15 @@ function parseClaims({ claim, value }: { claim: string; value: unknown }) { | ||||
| } | ||||
| 
 | ||||
| function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) { | ||||
|   return match(claim) | ||||
|     .with('exp', 'nbf', 'iat', () => dateFormatter(value)) | ||||
|     .with('alg', () => (_.isString(value) ? ALGORITHM_DESCRIPTIONS[value] : undefined)) | ||||
|     .otherwise(() => undefined); | ||||
|   if (['exp', 'nbf', 'iat'].includes(claim)) { | ||||
|     return dateFormatter(value); | ||||
|   } | ||||
| 
 | ||||
|   if (claim === 'alg' && _.isString(value)) { | ||||
|     return ALGORITHM_DESCRIPTIONS[value]; | ||||
|   } | ||||
| 
 | ||||
|   return undefined; | ||||
| } | ||||
| 
 | ||||
| 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 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> | ||||
| 
 | ||||
| <template> | ||||
|  | ||||
| @ -15,7 +15,7 @@ export { | ||||
| }; | ||||
| 
 | ||||
| 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) { | ||||
| @ -32,7 +32,7 @@ function base32toHex(base32: string) { | ||||
|     .map(value => base32Chars.indexOf(value).toString(2).padStart(5, '0')) | ||||
|     .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; | ||||
| } | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| <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 { copy: copyPrevious, copied: previousCopied } = useClipboard(); | ||||
| const { copy: copyCurrent, copied: currentCopied } = useClipboard(); | ||||
| const { copy: copyNext, copied: nextCopied } = useClipboard(); | ||||
| const { copy: copyPrevious, isJustCopied: previousCopied } = useCopy({ createToast: false }); | ||||
| const { copy: copyCurrent, isJustCopied: currentCopied } = useCopy({ createToast: false }); | ||||
| const { copy: copyNext, isJustCopied: nextCopied } = useCopy({ createToast: false }); | ||||
| 
 | ||||
| const { tokens } = toRefs(props); | ||||
| </script> | ||||
|  | ||||
| @ -14,6 +14,6 @@ test.describe('Tool - Password strength analyser', () => { | ||||
| 
 | ||||
|     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) { | ||||
|   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); | ||||
|   return exponent ? `${prettyBase}e${exponent}` : prettyBase; | ||||
| } | ||||
| @ -19,20 +19,20 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) { | ||||
|   } | ||||
| 
 | ||||
|   const timeUnits = [ | ||||
|     { unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation }, | ||||
|     { unit: 'century', secondsInUnit: 3153600000 }, | ||||
|     { unit: 'decade', secondsInUnit: 315360000 }, | ||||
|     { unit: 'year', secondsInUnit: 31536000 }, | ||||
|     { unit: 'month', secondsInUnit: 2592000 }, | ||||
|     { unit: 'week', secondsInUnit: 604800 }, | ||||
|     { unit: 'day', secondsInUnit: 86400 }, | ||||
|     { unit: 'hour', secondsInUnit: 3600 }, | ||||
|     { unit: 'minute', secondsInUnit: 60 }, | ||||
|     { unit: 'second', secondsInUnit: 1 }, | ||||
|     { unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation, plural: 'millennia' }, | ||||
|     { unit: 'century', secondsInUnit: 3153600000, plural: 'centuries' }, | ||||
|     { unit: 'decade', secondsInUnit: 315360000, plural: 'decades' }, | ||||
|     { unit: 'year', secondsInUnit: 31536000, plural: 'years' }, | ||||
|     { unit: 'month', secondsInUnit: 2592000, plural: 'months' }, | ||||
|     { unit: 'week', secondsInUnit: 604800, plural: 'weeks' }, | ||||
|     { unit: 'day', secondsInUnit: 86400, plural: 'days' }, | ||||
|     { unit: 'hour', secondsInUnit: 3600, plural: 'hours' }, | ||||
|     { unit: 'minute', secondsInUnit: 60, plural: 'minutes' }, | ||||
|     { unit: 'second', secondsInUnit: 1, plural: 'seconds' }, | ||||
|   ]; | ||||
| 
 | ||||
|   return _.chain(timeUnits) | ||||
|     .map(({ unit, secondsInUnit, format = _.identity }) => { | ||||
|     .map(({ unit, secondsInUnit, plural, format = _.identity }) => { | ||||
|       const quantity = Math.floor(seconds / secondsInUnit); | ||||
|       seconds %= secondsInUnit; | ||||
| 
 | ||||
| @ -41,7 +41,7 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) { | ||||
|       } | ||||
| 
 | ||||
|       const formattedQuantity = format(quantity); | ||||
|       return `${formattedQuantity} ${unit}${quantity > 1 ? 's' : ''}`; | ||||
|       return `${formattedQuantity} ${quantity > 1 ? plural : unit}`; | ||||
|     }) | ||||
|     .compact() | ||||
|     .take(2) | ||||
|  | ||||
| @ -36,7 +36,7 @@ const validationRoman = useValidation({ | ||||
| }); | ||||
| 
 | ||||
| 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> | ||||
| 
 | ||||
| <template> | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| <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 { useStyleStore } from '@/stores/style.store'; | ||||
| 
 | ||||
| const inputElement = ref<HTMLElement>(); | ||||
| const styleStore = useStyleStore(); | ||||
| const config = reactive<Partial<FormatFnOptions>>({ | ||||
| const config = reactive<FormatOptionsWithLanguage>({ | ||||
|   keywordCase: 'upper', | ||||
|   useTabs: false, | ||||
|   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 | ||||
|   alphabet?: string | ||||
| }) { | ||||
|   const allAlphabet | ||||
|     = alphabet | ||||
|     ?? [ | ||||
|       ...(withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : ''), | ||||
|       ...(withLowercase ? 'abcdefghijklmopqrstuvwxyz' : ''), | ||||
|       ...(withNumbers ? '0123456789' : ''), | ||||
|       ...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : ''), | ||||
|     ].join(''); | ||||
|   const allAlphabet = alphabet ?? [ | ||||
|     withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : '', | ||||
|     withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '', | ||||
|     withNumbers ? '0123456789' : '', | ||||
|     withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '', | ||||
|   ].join(''); ; | ||||
| 
 | ||||
|   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', | ||||
|   path: '/uuid-generator', | ||||
|   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'], | ||||
|   component: () => import('./uuid-generator.vue'), | ||||
|   icon: Fingerprint, | ||||
|  | ||||
| @ -34,7 +34,7 @@ const { copy } = useCopy({ source: uuids, text: 'UUIDs copied to the clipboard' | ||||
|     /> | ||||
| 
 | ||||
|     <div flex justify-center gap-3> | ||||
|       <c-button autofocus @click="copy"> | ||||
|       <c-button autofocus @click="copy()"> | ||||
|         Copy | ||||
|       </c-button> | ||||
|       <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"> | ||||
| import { useTheme } from './c-modal.theme'; | ||||
| 
 | ||||
| defineOptions({ | ||||
|   inheritAttrs: false, | ||||
| }); | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ open?: boolean; centered?: boolean }>(), { | ||||
|   open: false, | ||||
|   centered: true, | ||||
| }); | ||||
| 
 | ||||
| const emit = defineEmits(['update:open']); | ||||
| 
 | ||||
| const isOpen = useVModel(props, 'open', emit, { passive: true }); | ||||
| 
 | ||||
| const { centered } = toRefs(props); | ||||
| @ -29,10 +35,6 @@ defineExpose({ | ||||
|   isOpen, | ||||
| }); | ||||
| 
 | ||||
| defineOptions({ | ||||
|   inheritAttrs: false, | ||||
| }); | ||||
| 
 | ||||
| const theme = useTheme(); | ||||
| 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 { | ||||
|   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 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 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"], | ||||
|   "exclude": ["src/**/__tests__/*"], | ||||
|   "compilerOptions": { | ||||
|     "lib": ["ES2021"], | ||||
|     "lib": ["ES2022"], | ||||
|     "target": "es2022", | ||||
|     "module": "es2022", | ||||
|     "moduleResolution": "Node", | ||||
|     "composite": true, | ||||
|     "baseUrl": ".", | ||||
|     "paths": { | ||||
|       "@/*": ["./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.*"], | ||||
|   "compilerOptions": { | ||||
|     "composite": true, | ||||
|  | ||||
| @ -4,6 +4,6 @@ | ||||
|   "compilerOptions": { | ||||
|     "composite": true, | ||||
|     "lib": [], | ||||
|     "types": ["node", "jsdom"] | ||||
|     "types": ["node", "jsdom", "unplugin-icons/types/vue"] | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -25,7 +25,7 @@ export default defineConfig({ | ||||
|       runtimeOnly: true, | ||||
|       compositionOnly: true, | ||||
|       fullInstall: true, | ||||
|       include: [resolve(__dirname, 'locales/**'), resolve(__dirname, 'src/tools/*/locales/**')], | ||||
|       include: [resolve(__dirname, 'locales/**')], | ||||
|     }), | ||||
|     AutoImport({ | ||||
|       imports: [ | ||||
| @ -106,4 +106,7 @@ export default defineConfig({ | ||||
|   test: { | ||||
|     exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'], | ||||
|   }, | ||||
|   build: { | ||||
|     target: 'esnext', | ||||
|   }, | ||||
| }); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user