Merge branch 'CorentinTh:main' into tools/chmod-calculator

This commit is contained in:
Art051 2023-09-28 17:01:03 +01:00 committed by GitHub
commit fe45e0e923
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 2758 additions and 1567 deletions

View file

@ -10,5 +10,12 @@ module.exports = {
'@typescript-eslint/semi': ['error', 'always'], '@typescript-eslint/semi': ['error', 'always'],
'@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }], '@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
'vue/no-empty-component-block': ['error'], 'vue/no-empty-component-block': ['error'],
'no-restricted-imports': ['error', {
paths: [{
name: '@vueuse/core',
importNames: ['useClipboard'],
message: 'Please use local useCopy from src/composable/copy.ts instead of useClipboard.',
}],
}],
}, },
}; };

View file

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:

View file

@ -37,7 +37,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL

View file

@ -12,7 +12,7 @@ jobs:
outputs: outputs:
should_run: ${{ steps.should_run.outputs.should_run }} should_run: ${{ steps.should_run.outputs.should_run }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: print latest_commit - name: print latest_commit
run: echo ${{ github.sha }} run: echo ${{ github.sha }}
@ -28,7 +28,7 @@ jobs:
if: ${{ needs.check_date.outputs.should_run != 'false' }} if: ${{ needs.check_date.outputs.should_run != 'false' }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
@ -54,7 +54,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v2 uses: docker/login-action@v2

View file

@ -1,18 +1,18 @@
name: E2E tests name: E2E tests
on: [deployment_status] on:
pull_request:
push:
branches:
- main
jobs: jobs:
test: test:
if: github.event.deployment_status.state == 'success'
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
BASE_URL: ${{ github.event.deployment_status.target_url }}
strategy: strategy:
matrix: matrix:
shard: [1/3, 2/3, 3/3] shard: [1/3, 2/3, 3/3]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- run: corepack enable - run: corepack enable
@ -28,6 +28,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Build app
run: pnpm build
- name: Restore Playwright browsers from cache - name: Restore Playwright browsers from cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:

View file

@ -13,7 +13,7 @@ jobs:
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v2 uses: docker/login-action@v2
@ -55,7 +55,7 @@ jobs:
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- run: corepack enable - run: corepack enable

View file

@ -2,6 +2,124 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## Version 2023.08.21-6f93cba
### Features
- **copy**: support legacy copy to clipboard for older browser (#581) (6f93cba)
- **new tool**: string obfuscator (#575) (c58d6e3)
### Bug fixes
- **deps**: update dependency sql-formatter to v12 (#520) (2bcb77a)
### Chores
- **deps**: switched to fucking typescript v5 (#501) (76b2761)
- **deps**: update dependency @antfu/eslint-config to ^0.40.0 (#552) (6ff9a01)
- **deps**: update dependency prettier to v3 (#564) (a2b9b15)
- **deps**: removed @typescript-eslint/parser (#563) (144f86e)
- **deps**: removed ts-pattern (#565) (0f1f659)
## Version 2023.08.16-9bd4ad4
### Features
- **Case Converter**: Add lowercase and uppercase (#534) (7b6232a)
- **new tool**: emoji picker (#551) (93f7cf0)
- **ui**: added c-select in the ui lib (#550) (dfa1ba8)
- **new-tool**: password strength analyzer (#502) (a9c7b89)
- **new-tool**: yaml to toml (e29b258)
- **new-tool**: json to toml (ea50a3f)
- **new-tool**: toml to yaml (746e5bd)
- **new-tool**: toml to json (c7d4f11)
- **command-palette**: random tool action (ec4c533)
- **config**: allow app to run in a subfolder via BASE_URL (#461) (6304595)
- **new-tool**: percentage calculator (#456) (b9406a4)
- **new-tool**: json to csv converter (69f0bd0)
- **new tool**: xml formatter (#457) (a6bbeae)
- **chmod-calculator**: added symbolic representation (#455) (f771e7a)
- **enhancement**: use system dark mode (#458) (cf7b1f0)
- **phone-parser**: searchable country code select (d2956b6)
- **new tool**: camera screenshot and recorder (34d8e5c)
- **base64-string-converter**: switch to encode and decode url safe base64 strings (#392) (0b20f1c)
### Bug fixes
- **deps**: update dependency uuid to v9 (#566) (5e12991)
- **deps**: update dependency mathjs to v11 (#519) (7924456)
- **deps**: update dependency @vueuse/router to v10 (#516) (ea0f27c)
- **copy**: prevent shorthand copy if source is present in useCopy (#559) (86e964a)
- **c-lib**: hide component library shortcut link in non-dev (#557) (56d74d0)
- **emoji picker**: fix copy button (#556) (e5d0ba7)
- **deps**: update dependency @vueuse/head to v1 (#515) (d12dd40)
- **deps**: update dependency country-code-lookup to ^0.1.0 (#493) (8c72e69)
- **deps**: update dependency @vueuse/head to ^0.9.0 (#492) (cec9dea)
- **i18n**: fallback for demo i18n (12d9e5d)
- **typos**: fixed more typos & uppercase JSON (#475) (9526ed8)
- **about**: typos and wording (#474) (7068610)
- **mime-types**: typos (#470) (c4cec9e)
- **sonar**: took down minor sonar warning (4cbd7ac)
- **readme**: typo (105b21b)
- **ipv4-range-expander**: calculate correct for ip addresses where the first octet is lower than 128 (#405) (8c92d56)
- **ipv4-converter**: removed readonly on input (7aed9c5)
### Refactoring
- **navbar**: consistent spacing in navbar buttons (#507) (30f88fc)
- **ui**: remove n-text (#506) (72c98a3)
- **ui**: replaced some n-input to c-input (#505) (05ea545)
- **json-viewer**: input monospace font (#485) (9125dcf)
- **search**: command palette design (#463) (bcb98b3)
- **c-input-text**: force usage of props with default (1e2a35b)
- **naming**: prevent auto import conflicts for git memo (45c2474)
- **imports**: removed unnecessary imports to vue (fe61f0f)
- **ui**: removed all n-space (4d2b037)
- **ui**: replaced some n-input with c-input-text (f7fc779)
### Chores
- **deps**: update dependency vitest to ^0.34.0 (#562) (9bd4ad4)
- **deps**: update dependency node to v18.17.1 (#560) (65a9474)
- **deps**: update dependency unocss to ^0.55.0 (#561) (85cc7a8)
- **deps**: update dependency @unocss/eslint-config to ^0.55.0 (#553) (4268e25)
- **deps**: update dependency @intlify/unplugin-vue-i18n to ^0.12.0 (#526) (d1c8880)
- **deps**: update docker/login-action action to v2 (#512) (99bc84c)
- **deps**: update dependency jsdom to v22 (#499) (cd5a503)
- **deps**: update dependency @vitejs/plugin-vue-jsx to v3 (#497) (1a60236)
- **deps**: update dependency @vitejs/plugin-vue to v4 (#496) (a249421)
- **deps**: update dependency vite-plugin-pwa to ^0.16.0 (#488) (6498c9b)
- **deps**: update dependency vite to v4 (#503) (f40d7ec)
- **ci**: e2e against vercel deployement (#518) (2e28c50)
- **e2e**: execute e2e against built app (#511) (cf382b5)
- **deps**: update github/codeql-action action to v2 (#513) (0152583)
- **deps**: update node.js to v18 (#514) (38cb61d)
- **deps**: switched from vite-plugin-md to vite-plugin-vue-markdown (#510) (354aed6)
- **deps**: update dependency workbox-window to v7 (#509) (6b8682f)
- **deps**: update dependency vite-svg-loader to v4 (#508) (9e8349d)
- **deps**: update dependency typescript to ~4.9.0 (#481) (f440507)
- **deps**: update dependency vue-tsc to ^0.40.0 (#490) (b0d9a3e)
- **deps**: updated unplugin-auto-import (#504) (5c3bebf)
- **deps**: removed start-server-and-test dependency (8df7cd0)
- **deps**: update dependency c8 to v8 (#498) (6bda2ca)
- **deps**: update dependency @types/jsdom to v21 (#495) (994a1c3)
- **deps**: update node.js to v16.20.1 (#491) (05edaf4)
- **deps**: update dependency vitest to ^0.32.0 (#489) (49eacea)
- **deps**: update actions/checkout action to v3 (#494) (3f7d469)
- **deps**: update dependency unplugin-vue-components to ^0.25.0 (#484) (5f21908)
- **deps**: update dependency unplugin-auto-import to ^0.16.0 (#483) (6cb0845)
- **deps**: update dependency unocss to ^0.53.0 (#482) (38710dc)
- **deps**: update dependency @unocss/eslint-config to ^0.53.0 (#478) (282cfc4)
- **deps**: added renovate.json (#477) (363c2e4)
- **i18n**: tool scoped locales (#471) (1b038c7)
- **wysiwyg-editor**: update tiptap dependencies (732da08)
- **i18n**: setup i18n plugin config (ebfb872)
- **config**: netlify deployment support (#443) (93799af)
- **ci**: shard e2e tests (962a6d6)
- **lint**: switched to a better lint config (33c9b66)
### Refacor
- **transformers**: use monospace font for JSON and SQL text areas (#476) (ba4876d)
### Documentation
- **ide**: updated vscode extensions settings (#472) (847323c)
### Chors
- **deps**: updated vueuse dependency version (8515c24)
## Version 2023.05.14-77f2efc ## Version 2023.05.14-77f2efc
### Features ### Features

View file

@ -26,6 +26,7 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
**Other solutions:** **Other solutions:**
- [Cloudron](https://www.cloudron.io/store/tech.ittools.cloudron.html)
- [Tipi](https://www.runtipi.io/docs/apps-available) - [Tipi](https://www.runtipi.io/docs/apps-available)
- [Unraid](https://unraid.net/community/apps?q=it-tools) - [Unraid](https://unraid.net/community/apps?q=it-tools)
@ -34,6 +35,7 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
### Recommended IDE Setup ### Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) with the following extensions: [VSCode](https://code.visualstudio.com/) with the following extensions:
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) - [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur)
- [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). - [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
@ -41,16 +43,13 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
with the following settings: with the following settings:
```json5 ```json
{ {
"editor.formatOnSave": false, "editor.formatOnSave": false,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": true
}, },
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": ["locales", "src/tools/*/locales"],
"locales",
"src/tools/*/locales"
],
"i18n-ally.keystyle": "nested" "i18n-ally.keystyle": "nested"
} }
``` ```
@ -106,12 +105,20 @@ pnpm run script:create-new-tool my-tool-name
It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the imported tool in the proper category and develop the tool. It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the imported tool in the proper category and develop the tool.
## Contributors
Big thanks to all the people who have already contributed!
[![contributors](https://contrib.rocks/image?repo=corentinth/it-tools)](https://github.com/corentinth/it-tools/graphs/contributors)
## Credits ## Credits
Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr). Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr).
This project is continuously deployed using [vercel.com](https://vercel.com). This project is continuously deployed using [vercel.com](https://vercel.com).
Contributor graph is generated using [contrib.rocks](https://contrib.rocks/preview?repo=corentinth/it-tools).
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-it&#0045;tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT&#0032;Tools - Collection&#0032;of&#0032;handy&#0032;online&#0032;tools&#0032;for&#0032;devs&#0044;&#0032;with&#0032;great&#0032;UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-it&#0045;tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT&#0032;Tools - Collection&#0032;of&#0032;handy&#0032;online&#0032;tools&#0032;for&#0032;devs&#0044;&#0032;with&#0032;great&#0032;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&#0045;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&#0032;Tools - Collection&#0032;of&#0032;handy&#0032;online&#0032;tools&#0032;for&#0032;devs&#0044;&#0032;with&#0032;great&#0032;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&#0045;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&#0032;Tools - Collection&#0032;of&#0032;handy&#0032;online&#0032;tools&#0032;for&#0032;devs&#0044;&#0032;with&#0032;great&#0032;UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>

24
components.d.ts vendored
View file

@ -25,12 +25,17 @@ declare module '@vue/runtime-core' {
CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default'] CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default']
CButton: typeof import('./src/ui/c-button/c-button.vue')['default'] CButton: typeof import('./src/ui/c-button/c-button.vue')['default']
'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default'] 'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default']
CButtonsSelect: typeof import('./src/ui/c-buttons-select/c-buttons-select.vue')['default']
'CButtonsSelect.demo': typeof import('./src/ui/c-buttons-select/c-buttons-select.demo.vue')['default']
CCard: typeof import('./src/ui/c-card/c-card.vue')['default'] CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default'] 'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default']
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default'] ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default'] CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] 'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default']
CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default']
CKeyValueListItem: typeof import('./src/ui/c-key-value-list/c-key-value-list-item.vue')['default']
CLabel: typeof import('./src/ui/c-label/c-label.vue')['default'] CLabel: typeof import('./src/ui/c-label/c-label.vue')['default']
CLink: typeof import('./src/ui/c-link/c-link.vue')['default'] CLink: typeof import('./src/ui/c-link/c-link.vue')['default']
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] 'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
@ -44,8 +49,11 @@ declare module '@vue/runtime-core' {
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
CSelect: typeof import('./src/ui/c-select/c-select.vue')['default'] CSelect: typeof import('./src/ui/c-select/c-select.vue')['default']
'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default'] 'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default']
CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default']
'CTextCopyable.demo': typeof import('./src/ui/c-text-copyable/c-text-copyable.demo.vue')['default']
CTooltip: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default']
'CTooltip.demo': typeof import('./src/ui/c-tooltip/c-tooltip.demo.vue')['default']
DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default']
'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default']
'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default'] 'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default']
DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default']
DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default'] DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default']
@ -68,12 +76,12 @@ declare module '@vue/runtime-core' {
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default'] HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default'] HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] 'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] 'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
IconMdiCamera: typeof import('~icons/mdi/camera')['default'] IconMdiCamera: typeof import('~icons/mdi/camera')['default']
IconMdiCameraOutline: typeof import('~icons/mdi/camera-outline')['default']
IconMdiCameraVideoOff: typeof import('~icons/mdi/camera-video-off')['default']
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
IconMdiClose: typeof import('~icons/mdi/close')['default'] IconMdiClose: typeof import('~icons/mdi/close')['default']
@ -82,14 +90,11 @@ declare module '@vue/runtime-core' {
IconMdiDownload: typeof import('~icons/mdi/download')['default'] IconMdiDownload: typeof import('~icons/mdi/download')['default']
IconMdiEye: typeof import('~icons/mdi/eye')['default'] IconMdiEye: typeof import('~icons/mdi/eye')['default']
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
IconMdiMagnify: typeof import('~icons/mdi/magnify')['default']
IconMdiPause: typeof import('~icons/mdi/pause')['default'] IconMdiPause: typeof import('~icons/mdi/pause')['default']
IconMdiPlay: typeof import('~icons/mdi/play')['default'] IconMdiPlay: typeof import('~icons/mdi/play')['default']
IconMdiRecord: typeof import('~icons/mdi/record')['default'] IconMdiRecord: typeof import('~icons/mdi/record')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconMdiSearch: typeof import('~icons/mdi/search')['default'] IconMdiSearch: typeof import('~icons/mdi/search')['default']
IconMdiSearchRound: typeof import('~icons/mdi/search-round')['default']
IconMdiTea: typeof import('~icons/mdi/tea')['default']
IconMdiVideo: typeof import('~icons/mdi/video')['default'] IconMdiVideo: typeof import('~icons/mdi/video')['default']
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
@ -135,7 +140,6 @@ declare module '@vue/runtime-core' {
NH3: typeof import('naive-ui')['NH3'] NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon'] NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage'] NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup'] NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
NInputNumber: typeof import('naive-ui')['NInputNumber'] NInputNumber: typeof import('naive-ui')['NInputNumber']
@ -146,13 +150,11 @@ declare module '@vue/runtime-core' {
NPageHeader: typeof import('naive-ui')['NPageHeader'] NPageHeader: typeof import('naive-ui')['NPageHeader']
NProgress: typeof import('naive-ui')['NProgress'] NProgress: typeof import('naive-ui')['NProgress']
NScrollbar: typeof import('naive-ui')['NScrollbar'] NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSlider: typeof import('naive-ui')['NSlider'] NSlider: typeof import('naive-ui')['NSlider']
NStatistic: typeof import('naive-ui')['NStatistic'] NStatistic: typeof import('naive-ui')['NStatistic']
NSwitch: typeof import('naive-ui')['NSwitch'] NSwitch: typeof import('naive-ui')['NSwitch']
NTable: typeof import('naive-ui')['NTable'] NTable: typeof import('naive-ui')['NTable']
NTag: typeof import('naive-ui')['NTag'] NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip'] NTooltip: typeof import('naive-ui')['NTooltip']
NUpload: typeof import('naive-ui')['NUpload'] NUpload: typeof import('naive-ui')['NUpload']
NUploadDragger: typeof import('naive-ui')['NUploadDragger'] NUploadDragger: typeof import('naive-ui')['NUploadDragger']
@ -170,9 +172,11 @@ declare module '@vue/runtime-core' {
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default'] SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default'] SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default']
SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default'] SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default']
TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default'] TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default']
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default'] TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
TextDiff: typeof import('./src/tools/text-diff/text-diff.vue')['default']
TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default'] TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default']
TextToNatoAlphabet: typeof import('./src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue')['default'] TextToNatoAlphabet: typeof import('./src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue')['default']
TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default'] TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default']
@ -181,11 +185,13 @@ declare module '@vue/runtime-core' {
TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.vue')['default'] TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.vue')['default']
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default'] 'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
ToolCard: typeof import('./src/components/ToolCard.vue')['default'] ToolCard: typeof import('./src/components/ToolCard.vue')['default']
UlidGenerator: typeof import('./src/tools/ulid-generator/ulid-generator.vue')['default']
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default'] UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default'] UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default']
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default'] UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']
UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default'] UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default']
UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default'] UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default']
XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default'] XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default']
YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default'] YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default'] YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default']

View file

@ -1,6 +1,6 @@
{ {
"name": "it-tools", "name": "it-tools",
"version": "2023.5.14-77f2efc", "version": "2023.8.21-6f93cba",
"description": "Collection of handy online tools for developers, with great UX. ", "description": "Collection of handy online tools for developers, with great UX. ",
"keywords": [ "keywords": [
"productivity", "productivity",
@ -21,11 +21,12 @@
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && NODE_OPTIONS=--max_old_space_size=4096 vite build",
"preview": "vite preview --port 5050", "preview": "vite preview --port 5050",
"test": "npm run test:unit", "test": "npm run test:unit",
"test:unit": "vitest --environment jsdom", "test:unit": "vitest --environment jsdom",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"test:e2e:dev": "BASE_URL=http://localhost:5173 NO_WEB_SERVER=true playwright test",
"coverage": "vitest run --coverage", "coverage": "vitest run --coverage",
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", "typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
"lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore", "lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
@ -36,12 +37,12 @@
"@it-tools/bip39": "^0.0.4", "@it-tools/bip39": "^0.0.4",
"@it-tools/oggen": "^1.3.0", "@it-tools/oggen": "^1.3.0",
"@sindresorhus/slugify": "^2.2.0", "@sindresorhus/slugify": "^2.2.0",
"@tiptap/pm": "^2.0.3", "@tiptap/pm": "^2.1.6",
"@tiptap/starter-kit": "^2.0.3", "@tiptap/starter-kit": "^2.1.6",
"@tiptap/vue-3": "^2.0.3", "@tiptap/vue-3": "^2.0.3",
"@vicons/material": "^0.12.0", "@vicons/material": "^0.12.0",
"@vicons/tabler": "^0.12.0", "@vicons/tabler": "^0.12.0",
"@vueuse/core": "^10.1.2", "@vueuse/core": "^10.3.0",
"@vueuse/head": "^1.0.0", "@vueuse/head": "^1.0.0",
"@vueuse/router": "^10.0.0", "@vueuse/router": "^10.0.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
@ -58,12 +59,14 @@
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"iarna-toml-esm": "^3.0.5", "iarna-toml-esm": "^3.0.5",
"ibantools": "^4.3.3",
"json5": "^2.2.3", "json5": "^2.2.3",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.28", "libphonenumber-js": "^1.10.28",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mathjs": "^11.0.0", "mathjs": "^11.9.1",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"monaco-editor": "^0.43.0",
"naive-ui": "^2.34.3", "naive-ui": "^2.34.3",
"netmask": "^2.0.2", "netmask": "^2.0.2",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
@ -72,9 +75,9 @@
"plausible-tracker": "^0.3.8", "plausible-tracker": "^0.3.8",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"randombytes": "^2.1.0", "randombytes": "^2.1.0",
"sql-formatter": "^8.2.0", "sql-formatter": "^13.0.0",
"ts-pattern": "^4.2.2",
"ua-parser-js": "^1.0.35", "ua-parser-js": "^1.0.35",
"ulid": "^2.3.0",
"unicode-emoji-json": "^0.4.0", "unicode-emoji-json": "^0.4.0",
"unplugin-auto-import": "^0.16.4", "unplugin-auto-import": "^0.16.4",
"uuid": "^9.0.0", "uuid": "^9.0.0",
@ -86,44 +89,43 @@
"yaml": "^2.2.1" "yaml": "^2.2.1"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.39.3", "@antfu/eslint-config": "^0.41.0",
"@iconify-json/mdi": "^1.1.50", "@iconify-json/mdi": "^1.1.50",
"@intlify/unplugin-vue-i18n": "^0.12.0", "@intlify/unplugin-vue-i18n": "^0.13.0",
"@playwright/test": "^1.32.3", "@playwright/test": "^1.32.3",
"@rushstack/eslint-patch": "^1.2.0", "@rushstack/eslint-patch": "^1.2.0",
"@tsconfig/node18": "^18.2.0",
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/jsdom": "^21.0.0", "@types/jsdom": "^21.0.0",
"@types/lodash": "^4.14.192", "@types/lodash": "^4.14.192",
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",
"@types/netmask": "^2.0.0", "@types/netmask": "^2.0.0",
"@types/node": "^18.0.0", "@types/node": "^18.15.11",
"@types/node-forge": "^1.3.2", "@types/node-forge": "^1.3.2",
"@types/prettier": "^2.7.2",
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.0",
"@types/randombytes": "^2.0.0", "@types/randombytes": "^2.0.0",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",
"@types/uuid": "^9.0.0", "@types/uuid": "^9.0.0",
"@typescript-eslint/parser": "^5.58.0",
"@unocss/eslint-config": "^0.55.0", "@unocss/eslint-config": "^0.55.0",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.3.2",
"@vitejs/plugin-vue-jsx": "^3.0.0", "@vitejs/plugin-vue-jsx": "^3.0.2",
"@vue/compiler-sfc": "^3.2.47", "@vue/compiler-sfc": "^3.2.47",
"@vue/runtime-dom": "^3.3.4", "@vue/runtime-dom": "^3.3.4",
"@vue/test-utils": "^2.3.2", "@vue/test-utils": "^2.3.2",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.4.0",
"c8": "^8.0.0", "c8": "^8.0.0",
"consola": "^3.0.2", "consola": "^3.0.2",
"eslint": "^8.38.0", "eslint": "^8.47.0",
"jsdom": "^22.0.0", "jsdom": "^22.0.0",
"less": "^4.1.3", "less": "^4.1.3",
"prettier": "^2.8.7", "prettier": "^3.0.0",
"typescript": "~4.9.0", "typescript": "~5.2.0",
"unocss": "^0.55.0", "unocss": "^0.55.0",
"unocss-preset-scrollbar": "^0.2.1", "unocss-preset-scrollbar": "^0.2.1",
"unplugin-icons": "^0.16.1", "unplugin-icons": "^0.17.0",
"unplugin-vue-components": "^0.25.0", "unplugin-vue-components": "^0.25.0",
"vite": "^4.0.0", "vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.0", "vite-plugin-pwa": "^0.16.0",
"vite-plugin-vue-markdown": "^0.23.5", "vite-plugin-vue-markdown": "^0.23.5",
"vite-svg-loader": "^4.0.0", "vite-svg-loader": "^4.0.0",

View file

@ -2,6 +2,7 @@ import { defineConfig, devices } from '@playwright/test';
const isCI = !!process.env.CI; const isCI = !!process.env.CI;
const baseUrl = process.env.BASE_URL || 'http://localhost:5050'; const baseUrl = process.env.BASE_URL || 'http://localhost:5050';
const useWebServer = process.env.NO_WEB_SERVER !== 'true';
/** /**
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.
@ -52,13 +53,13 @@ export default defineConfig({
/* Run your local dev server before starting the tests */ /* Run your local dev server before starting the tests */
...(isCI ...(useWebServer
? {} && {
: { webServer: {
webServer: { command: 'npm run preview',
command: 'npm run preview', url: 'http://127.0.0.1:5050',
url: 'http://127.0.0.1:5050', reuseExistingServer: !isCI,
reuseExistingServer: true, },
}, }
}), ),
}); });

2828
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { useClipboard, useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { useCopy } from '@/composable/copy';
const props = defineProps<{ value: string }>(); const props = defineProps<{ value: string }>();
const emit = defineEmits(['update:value']); const emit = defineEmits(['update:value']);
const value = useVModel(props, 'value', emit); const value = useVModel(props, 'value', emit);
const tooltipText = ref('Copy to clipboard'); const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : 'Copy to clipboard');
const { copy } = useClipboard({ source: value });
function onCopyClicked() {
copy();
tooltipText.value = 'Copied!';
setTimeout(() => {
tooltipText.value = 'Copy to clipboard';
}, 2000);
}
</script> </script>
<template> <template>
@ -24,7 +15,7 @@ function onCopyClicked() {
<template #suffix> <template #suffix>
<n-tooltip trigger="hover"> <n-tooltip trigger="hover">
<template #trigger> <template #trigger>
<c-button circle variant="text" size="small" @click="onCopyClicked"> <c-button circle variant="text" size="small" @click="copy()">
<icon-mdi-content-copy /> <icon-mdi-content-copy />
</c-button> </c-button>
</template> </template>

View file

@ -1,26 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { useClipboard } from '@vueuse/core'; import { useCopy } from '@/composable/copy';
const props = withDefaults(defineProps<{ value?: string }>(), { value: '' }); const props = withDefaults(defineProps<{ value?: string }>(), { value: '' });
const { value } = toRefs(props); const { value } = toRefs(props);
const initialText = 'Copy to clipboard'; const initialText = 'Copy to clipboard';
const tooltipText = ref(initialText);
const { copy } = useClipboard({ source: value }); const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : initialText);
function handleClick() {
copy();
tooltipText.value = 'Copied!';
setTimeout(() => (tooltipText.value = initialText), 1000);
}
</script> </script>
<template> <template>
<n-tooltip trigger="hover"> <n-tooltip trigger="hover">
<template #trigger> <template #trigger>
<span class="value" @click="handleClick">{{ value }}</span> <span class="value" @click="copy()">{{ value }}</span>
</template> </template>
{{ tooltipText }} {{ tooltipText }}
</n-tooltip> </n-tooltip>

View file

@ -1,12 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { Copy } from '@vicons/tabler'; import { Copy } from '@vicons/tabler';
import { useClipboard, useElementSize } from '@vueuse/core'; import { useElementSize } from '@vueuse/core';
import hljs from 'highlight.js/lib/core'; import hljs from 'highlight.js/lib/core';
import jsonHljs from 'highlight.js/lib/languages/json'; import jsonHljs from 'highlight.js/lib/languages/json';
import sqlHljs from 'highlight.js/lib/languages/sql'; import sqlHljs from 'highlight.js/lib/languages/sql';
import xmlHljs from 'highlight.js/lib/languages/xml'; import xmlHljs from 'highlight.js/lib/languages/xml';
import yamlHljs from 'highlight.js/lib/languages/yaml'; import yamlHljs from 'highlight.js/lib/languages/yaml';
import iniHljs from 'highlight.js/lib/languages/ini'; import iniHljs from 'highlight.js/lib/languages/ini';
import { useCopy } from '@/composable/copy';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -33,17 +34,8 @@ hljs.registerLanguage('toml', iniHljs);
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) }; const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) };
const { copy } = useClipboard({ source: value }); const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
const tooltipText = ref(copyMessage.value); const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.value);
function onCopyClicked() {
copy();
tooltipText.value = 'Copied !';
setTimeout(() => {
tooltipText.value = copyMessage.value;
}, 2000);
}
</script> </script>
<template> <template>
@ -61,7 +53,7 @@ function onCopyClicked() {
<n-tooltip v-if="value" trigger="hover"> <n-tooltip v-if="value" trigger="hover">
<template #trigger> <template #trigger>
<div class="copy-button" :class="[copyPlacement]"> <div class="copy-button" :class="[copyPlacement]">
<c-button circle important:h-10 important:w-10 @click="onCopyClicked"> <c-button circle important:h-10 important:w-10 @click="copy()">
<n-icon size="22" :component="Copy" /> <n-icon size="22" :component="Copy" />
</c-button> </c-button>
</div> </div>
@ -70,7 +62,7 @@ function onCopyClicked() {
</n-tooltip> </n-tooltip>
</c-card> </c-card>
<div v-if="copyPlacement === 'outside'" mt-4 flex justify-center> <div v-if="copyPlacement === 'outside'" mt-4 flex justify-center>
<c-button @click="onCopyClicked"> <c-button @click="copy()">
{{ tooltipText }} {{ tooltipText }}
</c-button> </c-button>
</div> </div>

View file

@ -1,11 +1,19 @@
import { type MaybeRef, get, useClipboard } from '@vueuse/core'; // eslint-disable-next-line no-restricted-imports
import { useClipboard } from '@vueuse/core';
import { useMessage } from 'naive-ui'; import { useMessage } from 'naive-ui';
import type { MaybeRefOrGetter } from 'vue';
export function useCopy({ source, text = 'Copied to the clipboard', createToast = true }: { source?: MaybeRefOrGetter<string>; text?: string; createToast?: boolean } = {}) {
const { copy, copied, ...rest } = useClipboard({
source,
legacy: true,
});
export function useCopy({ source, text = 'Copied to the clipboard' }: { source?: MaybeRef<unknown>; text?: string } = {}) {
const { copy } = useClipboard(source ? { source: computed(() => String(get(source))) } : {});
const message = useMessage(); const message = useMessage();
return { return {
...rest,
isJustCopied: copied,
async copy(content?: string, { notificationMessage }: { notificationMessage?: string } = {}) { async copy(content?: string, { notificationMessage }: { notificationMessage?: string } = {}) {
if (source) { if (source) {
await copy(); await copy();
@ -14,7 +22,9 @@ export function useCopy({ source, text = 'Copied to the clipboard' }: { source?:
await copy(content); await copy(content);
} }
message.success(notificationMessage ?? text); if (createToast) {
message.success(notificationMessage ?? text);
}
}, },
}; };
} }

View file

@ -49,7 +49,7 @@ const { t } = useI18n();
</transition> </transition>
<div v-if="toolStore.newTools.length > 0"> <div v-if="toolStore.newTools.length > 0">
<n-h3>{{ t('home.categories.newestTools', 'Newest tools') }}</n-h3> <n-h3>{{ t('home.categories.newestTools') }}</n-h3>
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
<n-gi v-for="tool in toolStore.newTools" :key="tool.name"> <n-gi v-for="tool in toolStore.newTools" :key="tool.name">
<ToolCard :tool="tool" /> <ToolCard :tool="tool" />

View file

@ -1,6 +1,22 @@
import type { App } from 'vue'; import type { Plugin } from 'vue';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import messages from '@intlify/unplugin-vue-i18n/messages'; import baseMessages from '@intlify/unplugin-vue-i18n/messages';
import _ from 'lodash';
import { parse as parseYaml } from 'yaml';
const i18nFiles = import.meta.glob('../tools/*/locales/**.yml', { as: 'raw' });
const messagesByTools = await Promise.all(_.map(i18nFiles, async (fileDescriptor, path) => {
const [, locale] = path.match(/\.\/tools\/.*?\/locales\/(.*)\.ya?ml$/i) ?? [];
const content = parseYaml(await fileDescriptor());
return { [locale]: content };
}));
const messages = _.merge(
baseMessages,
_.merge({}, ...messagesByTools),
);
const i18n = createI18n({ const i18n = createI18n({
legacy: false, legacy: false,
@ -8,8 +24,8 @@ const i18n = createI18n({
messages, messages,
}); });
export const i18nPlugin = { export const i18nPlugin: Plugin = {
install: (app: App) => { install: (app) => {
app.use(i18n); app.use(i18n);
}, },
}; };

28
src/shims.d.ts vendored
View file

@ -1,21 +1,35 @@
declare module '*.vue' { declare module '*.vue' {
import type { ComponentOptions, ComponentOptions } from 'vue'; import type { ComponentOptions } from 'vue';
const Component: ComponentOptions; const Component: ComponentOptions;
export default Component; export default Component;
} }
declare module '*.md' { declare module '*.md' {
import type { ComponentOptions } from 'vue';
const Component: ComponentOptions; const Component: ComponentOptions;
export default Component; export default Component;
} }
declare module '~icons/*' {
import { FunctionalComponent, SVGAttributes } from 'vue';
const component: FunctionalComponent<SVGAttributes>;
export default component;
}
declare module 'iarna-toml-esm' { declare module 'iarna-toml-esm' {
export const parse: (toml: string) => any; export const parse: (toml: string) => any;
export const stringify: (obj: any) => string; export const stringify: (obj: any) => string;
} }
declare module 'emojilib' {
const lib: Record<string, string[]>;
export default lib;
}
declare module 'unicode-emoji-json' {
const emoji: Record<string, {
name: string;
slug: string;
group: string;
emoji_version: string;
unicode_version: string;
skin_tone_support: boolean;
skin_tone_support_unicode_version: string;
}>;
export default emoji;
}

View file

@ -45,7 +45,7 @@ const compareMatch = computed(() => compareSync(compareString.value, compareHash
<c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text /> <c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text />
</n-form-item> </n-form-item>
<n-form-item label="Your hash: " label-placement="left"> <n-form-item label="Your hash: " label-placement="left">
<c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text /> <c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text />
</n-form-item> </n-form-item>
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
<div class="compare-result" :class="{ positive: compareMatch }"> <div class="compare-result" :class="{ positive: compareMatch }">

View file

@ -18,7 +18,7 @@ function computeVariance({ data }: { data: number[] }) {
return computeAverage({ data: squaredDiffs }); return computeAverage({ data: squaredDiffs });
} }
function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; headerMap?: Record<string, string> }) { function arrayToMarkdownTable({ data, headerMap = {} }: { data: Record<string, unknown>[]; headerMap?: Record<string, string> }) {
if (!Array.isArray(data) || data.length === 0) { if (!Array.isArray(data) || data.length === 0) {
return ''; return '';
} }

View file

@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { Plus, Trash } from '@vicons/tabler'; import { Plus, Trash } from '@vicons/tabler';
import { useClipboard, useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import _ from 'lodash'; import _ from 'lodash';
import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models'; import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models';
import DynamicValues from './dynamic-values.vue'; import DynamicValues from './dynamic-values.vue';
import { useCopy } from '@/composable/copy';
const suites = useStorage('benchmark-builder:suites', [ const suites = useStorage('benchmark-builder:suites', [
{ title: 'Suite 1', data: [5, 10] }, { title: 'Suite 1', data: [5, 10] },
@ -47,7 +48,7 @@ const results = computed(() => {
}); });
}); });
const { copy } = useClipboard(); const { copy } = useCopy({ createToast: false });
const header = { const header = {
title: 'Suite', title: 'Suite',

View file

@ -92,7 +92,7 @@ const inputLabelAlignmentConfig = {
v-bind="inputLabelAlignmentConfig" v-bind="inputLabelAlignmentConfig"
/> />
<div divider my-16px /> <div my-16px divider />
<InputCopyable <InputCopyable
v-for="format in formats" v-for="format in formats"

View file

@ -82,7 +82,7 @@ const formats: DateFormat[] = [
{ {
name: 'Mongo ObjectID', name: 'Mongo ObjectID',
fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`, fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`,
toDate: objectId => new Date(parseInt(objectId.substring(0, 8), 16) * 1000), toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000),
formatMatcher: date => isMongoObjectId(date), formatMatcher: date => isMongoObjectId(date),
}, },
]; ];
@ -146,7 +146,7 @@ function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date
<c-input-text <c-input-text
v-model:value="inputDate" v-model:value="inputDate"
autofocus autofocus
placeholder="Put you date string here..." placeholder="Put your date string here..."
clearable clearable
test-id="date-time-converter-input" test-id="date-time-converter-input"
:validation="validation" :validation="validation"

View file

@ -29,7 +29,7 @@ const { copy } = useCopy();
Unicode: <span border="1px solid current op-30" b-rd-xl px-12px py-4px>{{ emojiInfo.unicode }}</span> Unicode: <span border="1px solid current op-30" b-rd-xl px-12px py-4px>{{ emojiInfo.unicode }}</span>
</div> --> </div> -->
<div flex gap-2 font-mono text-xs op-70> <div flex gap-2 text-xs font-mono op-70>
<span cursor-pointer transition hover:text-primary @click="copy(emojiInfo.codePoints, { notificationMessage: `Code points '${emojiInfo.codePoints}' copied to the clipboard` })"> <span cursor-pointer transition hover:text-primary @click="copy(emojiInfo.codePoints, { notificationMessage: `Code points '${emojiInfo.codePoints}' copied to the clipboard` })">
{{ emojiInfo.codePoints }} {{ emojiInfo.codePoints }}
</span> </span>

View file

@ -5,4 +5,4 @@ export type EmojiInfo = {
emoji: string emoji: string
codePoints: string | undefined codePoints: string | undefined
unicode: string unicode: string
} & typeof emojiUnicodeData['\uD83E\uDD10']; } & typeof emojiUnicodeData[string];

View file

@ -2,6 +2,6 @@ export function convertHexToBin(hex: string) {
return hex return hex
.trim() .trim()
.split('') .split('')
.map(byte => parseInt(byte, 16).toString(2).padStart(4, '0')) .map(byte => Number.parseInt(byte, 16).toString(2).padStart(4, '0'))
.join(''); .join('');
} }

View file

@ -1,14 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { format } from 'prettier'; import { format } from 'prettier';
import htmlParser from 'prettier/parser-html'; import htmlParser from 'prettier/plugins/html';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import Editor from './editor/editor.vue'; import Editor from './editor/editor.vue';
import TextareaCopyable from '@/components/TextareaCopyable.vue'; import TextareaCopyable from '@/components/TextareaCopyable.vue';
const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>'); const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>');
const formattedHtml = asyncComputed(() => format(html.value, { parser: 'html', plugins: [htmlParser] }), '');
</script> </script>
<template> <template>
<Editor v-model:html="html" /> <Editor v-model:html="html" />
<TextareaCopyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" /> <TextareaCopyable :value="formattedHtml" language="html" />
</template> </template>

View file

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

View file

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

View file

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

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

View file

@ -1,6 +1,10 @@
import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter'; import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator'; import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as ulidGenerator } from './ulid-generator';
import { tool as ibanValidatorAndParser } from './iban-validator-and-parser';
import { tool as stringObfuscator } from './string-obfuscator';
import { tool as textDiff } from './text-diff';
import { tool as emojiPicker } from './emoji-picker'; import { tool as emojiPicker } from './emoji-picker';
import { tool as passwordStrengthAnalyser } from './password-strength-analyser'; import { tool as passwordStrengthAnalyser } from './password-strength-analyser';
import { tool as yamlToToml } from './yaml-to-toml'; import { tool as yamlToToml } from './yaml-to-toml';
@ -53,6 +57,7 @@ import { tool as metaTagGenerator } from './meta-tag-generator';
import { tool as mimeTypes } from './mime-types'; import { tool as mimeTypes } from './mime-types';
import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator'; import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
import { tool as qrCodeGenerator } from './qr-code-generator'; import { tool as qrCodeGenerator } from './qr-code-generator';
import { tool as wifiQrCodeGenerator } from './wifi-qr-code-generator';
import { tool as randomPortGenerator } from './random-port-generator'; import { tool as randomPortGenerator } from './random-port-generator';
import { tool as romanNumeralConverter } from './roman-numeral-converter'; import { tool as romanNumeralConverter } from './roman-numeral-converter';
import { tool as sqlPrettify } from './sql-prettify'; import { tool as sqlPrettify } from './sql-prettify';
@ -70,7 +75,7 @@ import { tool as xmlFormatter } from './xml-formatter';
export const toolsByCategory: ToolCategory[] = [ export const toolsByCategory: ToolCategory[] = [
{ {
name: 'Crypto', name: 'Crypto',
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser], components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser],
}, },
{ {
name: 'Converter', name: 'Converter',
@ -114,7 +119,7 @@ export const toolsByCategory: ToolCategory[] = [
}, },
{ {
name: 'Images and videos', name: 'Images and videos',
components: [qrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
}, },
{ {
name: 'Development', name: 'Development',
@ -145,11 +150,11 @@ export const toolsByCategory: ToolCategory[] = [
}, },
{ {
name: 'Text', name: 'Text',
components: [loremIpsumGenerator, textStatistics, emojiPicker], components: [loremIpsumGenerator, textStatistics, emojiPicker, stringObfuscator, textDiff],
}, },
{ {
name: 'Data', name: 'Data',
components: [phoneParserAndFormatter], components: [phoneParserAndFormatter, ibanValidatorAndParser],
}, },
]; ];

View file

@ -23,7 +23,7 @@ function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: str
+ _.chain(ip) + _.chain(ip)
.trim() .trim()
.split('.') .split('.')
.map(part => parseInt(part).toString(16).padStart(2, '0')) .map(part => Number.parseInt(part).toString(16).padStart(2, '0'))
.chunk(2) .chunk(2)
.map(blocks => blocks.join('')) .map(blocks => blocks.join(''))
.join(':') .join(':')

View file

@ -13,7 +13,7 @@ function getRangesize(start: string, end: string) {
return -1; return -1;
} }
return 1 + parseInt(end, 2) - parseInt(start, 2); return 1 + Number.parseInt(end, 2) - Number.parseInt(start, 2);
} }
function getCidr(start: string, end: string) { function getCidr(start: string, end: string) {
@ -55,8 +55,8 @@ function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) {
const cidr = getCidr(start, end); const cidr = getCidr(start, end);
if (cidr != null) { if (cidr != null) {
const result: Ipv4RangeExpanderResult = {}; const result: Ipv4RangeExpanderResult = {};
result.newEnd = bits2ip(parseInt(cidr.end, 2)); result.newEnd = bits2ip(Number.parseInt(cidr.end, 2));
result.newStart = bits2ip(parseInt(cidr.start, 2)); result.newStart = bits2ip(Number.parseInt(cidr.start, 2));
result.newCidr = `${result.newStart}/${cidr.mask}`; result.newCidr = `${result.newStart}/${cidr.mask}`;
result.newSize = getRangesize(cidr.start, cidr.end); result.newSize = getRangesize(cidr.start, cidr.end);

View file

@ -1,6 +1,5 @@
import jwtDecode, { type JwtHeader, type JwtPayload } from 'jwt-decode'; import jwtDecode, { type JwtHeader, type JwtPayload } from 'jwt-decode';
import _ from 'lodash'; import _ from 'lodash';
import { match } from 'ts-pattern';
import { ALGORITHM_DESCRIPTIONS, CLAIM_DESCRIPTIONS } from './jwt-parser.constants'; import { ALGORITHM_DESCRIPTIONS, CLAIM_DESCRIPTIONS } from './jwt-parser.constants';
export { decodeJwt }; export { decodeJwt };
@ -32,10 +31,15 @@ function parseClaims({ claim, value }: { claim: string; value: unknown }) {
} }
function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) { function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) {
return match(claim) if (['exp', 'nbf', 'iat'].includes(claim)) {
.with('exp', 'nbf', 'iat', () => dateFormatter(value)) return dateFormatter(value);
.with('alg', () => (_.isString(value) ? ALGORITHM_DESCRIPTIONS[value] : undefined)) }
.otherwise(() => undefined);
if (claim === 'alg' && _.isString(value)) {
return ALGORITHM_DESCRIPTIONS[value];
}
return undefined;
} }
function dateFormatter(value: unknown) { function dateFormatter(value: unknown) {

View file

@ -8,7 +8,7 @@ const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '')
const macAddress = ref('20:37:06:12:34:56'); const macAddress = ref('20:37:06:12:34:56');
const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]); const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]);
const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' }); const { copy } = useCopy({ source: () => details.value ?? '', text: 'Vendor info copied to the clipboard' });
</script> </script>
<template> <template>

View file

@ -15,7 +15,7 @@ export {
}; };
function hexToBytes(hex: string) { function hexToBytes(hex: string) {
return (hex.match(/.{1,2}/g) ?? []).map(char => parseInt(char, 16)); return (hex.match(/.{1,2}/g) ?? []).map(char => Number.parseInt(char, 16));
} }
function computeHMACSha1(message: string, key: string) { function computeHMACSha1(message: string, key: string) {
@ -32,7 +32,7 @@ function base32toHex(base32: string) {
.map(value => base32Chars.indexOf(value).toString(2).padStart(5, '0')) .map(value => base32Chars.indexOf(value).toString(2).padStart(5, '0'))
.join(''); .join('');
const hex = (bits.match(/.{1,8}/g) ?? []).map(chunk => parseInt(chunk, 2).toString(16).padStart(2, '0')).join(''); const hex = (bits.match(/.{1,8}/g) ?? []).map(chunk => Number.parseInt(chunk, 2).toString(16).padStart(2, '0')).join('');
return hex; return hex;
} }

View file

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { useClipboard } from '@vueuse/core'; import { useCopy } from '@/composable/copy';
const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>(); const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>();
const { copy: copyPrevious, copied: previousCopied } = useClipboard(); const { copy: copyPrevious, isJustCopied: previousCopied } = useCopy({ createToast: false });
const { copy: copyCurrent, copied: currentCopied } = useClipboard(); const { copy: copyCurrent, isJustCopied: currentCopied } = useCopy({ createToast: false });
const { copy: copyNext, copied: nextCopied } = useClipboard(); const { copy: copyNext, isJustCopied: nextCopied } = useCopy({ createToast: false });
const { tokens } = toRefs(props); const { tokens } = toRefs(props);
</script> </script>

View file

@ -14,6 +14,6 @@ test.describe('Tool - Password strength analyser', () => {
const crackDuration = await page.getByTestId('crack-duration').textContent(); const crackDuration = await page.getByTestId('crack-duration').textContent();
expect(crackDuration).toEqual('15,091 milleniums, 3 centurys'); expect(crackDuration).toEqual('15,091 millennia, 3 centuries');
}); });
}); });

View file

@ -4,7 +4,7 @@ export { getPasswordCrackTimeEstimation, getCharsetLength };
function prettifyExponentialNotation(exponentialNotation: number) { function prettifyExponentialNotation(exponentialNotation: number) {
const [base, exponent] = exponentialNotation.toString().split('e'); const [base, exponent] = exponentialNotation.toString().split('e');
const baseAsNumber = parseFloat(base); const baseAsNumber = Number.parseFloat(base);
const prettyBase = baseAsNumber % 1 === 0 ? baseAsNumber.toLocaleString() : baseAsNumber.toFixed(2); const prettyBase = baseAsNumber % 1 === 0 ? baseAsNumber.toLocaleString() : baseAsNumber.toFixed(2);
return exponent ? `${prettyBase}e${exponent}` : prettyBase; return exponent ? `${prettyBase}e${exponent}` : prettyBase;
} }
@ -19,20 +19,20 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) {
} }
const timeUnits = [ const timeUnits = [
{ unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation }, { unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation, plural: 'millennia' },
{ unit: 'century', secondsInUnit: 3153600000 }, { unit: 'century', secondsInUnit: 3153600000, plural: 'centuries' },
{ unit: 'decade', secondsInUnit: 315360000 }, { unit: 'decade', secondsInUnit: 315360000, plural: 'decades' },
{ unit: 'year', secondsInUnit: 31536000 }, { unit: 'year', secondsInUnit: 31536000, plural: 'years' },
{ unit: 'month', secondsInUnit: 2592000 }, { unit: 'month', secondsInUnit: 2592000, plural: 'months' },
{ unit: 'week', secondsInUnit: 604800 }, { unit: 'week', secondsInUnit: 604800, plural: 'weeks' },
{ unit: 'day', secondsInUnit: 86400 }, { unit: 'day', secondsInUnit: 86400, plural: 'days' },
{ unit: 'hour', secondsInUnit: 3600 }, { unit: 'hour', secondsInUnit: 3600, plural: 'hours' },
{ unit: 'minute', secondsInUnit: 60 }, { unit: 'minute', secondsInUnit: 60, plural: 'minutes' },
{ unit: 'second', secondsInUnit: 1 }, { unit: 'second', secondsInUnit: 1, plural: 'seconds' },
]; ];
return _.chain(timeUnits) return _.chain(timeUnits)
.map(({ unit, secondsInUnit, format = _.identity }) => { .map(({ unit, secondsInUnit, plural, format = _.identity }) => {
const quantity = Math.floor(seconds / secondsInUnit); const quantity = Math.floor(seconds / secondsInUnit);
seconds %= secondsInUnit; seconds %= secondsInUnit;
@ -41,7 +41,7 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) {
} }
const formattedQuantity = format(quantity); const formattedQuantity = format(quantity);
return `${formattedQuantity} ${unit}${quantity > 1 ? 's' : ''}`; return `${formattedQuantity} ${quantity > 1 ? plural : unit}`;
}) })
.compact() .compact()
.take(2) .take(2)

View file

@ -36,7 +36,7 @@ const validationRoman = useValidation({
}); });
const { copy: copyRoman } = useCopy({ source: outputRoman, text: 'Roman number copied to the clipboard' }); const { copy: copyRoman } = useCopy({ source: outputRoman, text: 'Roman number copied to the clipboard' });
const { copy: copyArabic } = useCopy({ source: outputNumeral, text: 'Arabic number copied to the clipboard' }); const { copy: copyArabic } = useCopy({ source: () => String(outputNumeral), text: 'Arabic number copied to the clipboard' });
</script> </script>
<template> <template>

View file

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { type FormatFnOptions, format as formatSQL } from 'sql-formatter'; import { type FormatOptionsWithLanguage, format as formatSQL } from 'sql-formatter';
import TextareaCopyable from '@/components/TextareaCopyable.vue'; import TextareaCopyable from '@/components/TextareaCopyable.vue';
import { useStyleStore } from '@/stores/style.store'; import { useStyleStore } from '@/stores/style.store';
const inputElement = ref<HTMLElement>(); const inputElement = ref<HTMLElement>();
const styleStore = useStyleStore(); const styleStore = useStyleStore();
const config = reactive<Partial<FormatFnOptions>>({ const config = reactive<FormatOptionsWithLanguage>({
keywordCase: 'upper', keywordCase: 'upper',
useTabs: false, useTabs: false,
language: 'sql', language: 'sql',

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

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

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

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

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

View file

@ -0,0 +1,5 @@
<template>
<c-card w-full important:flex-1 important:pa-0>
<c-diff-editor />
</c-card>
</template>

View file

@ -15,14 +15,12 @@ export function createToken({
length?: number length?: number
alphabet?: string alphabet?: string
}) { }) {
const allAlphabet const allAlphabet = alphabet ?? [
= alphabet withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : '',
?? [ withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '',
...(withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : ''), withNumbers ? '0123456789' : '',
...(withLowercase ? 'abcdefghijklmopqrstuvwxyz' : ''), withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '',
...(withNumbers ? '0123456789' : ''), ].join(''); ;
...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : ''),
].join('');
return shuffleString(allAlphabet.repeat(length)).substring(0, length); return shuffleString(allAlphabet.repeat(length)).substring(0, length);
} }

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

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

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

View file

@ -5,7 +5,7 @@ export const tool = defineTool({
name: 'UUIDs v4 generator', name: 'UUIDs v4 generator',
path: '/uuid-generator', path: '/uuid-generator',
description: description:
'A universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot !).', 'A Universally Unique Identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot!).',
keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'], keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'],
component: () => import('./uuid-generator.vue'), component: () => import('./uuid-generator.vue'),
icon: Fingerprint, icon: Fingerprint,

View file

@ -34,7 +34,7 @@ const { copy } = useCopy({ source: uuids, text: 'UUIDs copied to the clipboard'
/> />
<div flex justify-center gap-3> <div flex justify-center gap-3>
<c-button autofocus @click="copy"> <c-button autofocus @click="copy()">
Copy Copy
</c-button> </c-button>
<c-button @click="refreshUUIDs"> <c-button @click="refreshUUIDs">

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

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

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

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

View file

@ -0,0 +1,5 @@
import type { CSelectOption } from '../c-select/c-select.types';
export type CButtonSelectOption<T> = CSelectOption<T> & {
tooltip?: string
};

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

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

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

View 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[];

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

View file

@ -1,11 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTheme } from './c-modal.theme'; import { useTheme } from './c-modal.theme';
defineOptions({
inheritAttrs: false,
});
const props = withDefaults(defineProps<{ open?: boolean; centered?: boolean }>(), { const props = withDefaults(defineProps<{ open?: boolean; centered?: boolean }>(), {
open: false, open: false,
centered: true, centered: true,
}); });
const emit = defineEmits(['update:open']); const emit = defineEmits(['update:open']);
const isOpen = useVModel(props, 'open', emit, { passive: true }); const isOpen = useVModel(props, 'open', emit, { passive: true });
const { centered } = toRefs(props); const { centered } = toRefs(props);
@ -29,10 +35,6 @@ defineExpose({
isOpen, isOpen,
}); });
defineOptions({
inheritAttrs: false,
});
const theme = useTheme(); const theme = useTheme();
const modal = ref(); const modal = ref();

View file

@ -0,0 +1,3 @@
<template>
<c-text-copyable value="value" displayed-value="displayedValue" />
</template>

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

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

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

View file

@ -4,7 +4,7 @@ const clampHex = (value: number) => Math.max(0, Math.min(255, Math.round(value))
function lighten(color: string, amount: number): string { function lighten(color: string, amount: number): string {
const alpha = color.length === 9 ? color.slice(7) : ''; const alpha = color.length === 9 ? color.slice(7) : '';
const num = parseInt(color.slice(1, 7), 16); const num = Number.parseInt(color.slice(1, 7), 16);
const r = clampHex(((num >> 16) & 255) + amount); const r = clampHex(((num >> 16) & 255) + amount);
const g = clampHex(((num >> 8) & 255) + amount); const g = clampHex(((num >> 8) & 255) + amount);

View file

@ -7,5 +7,5 @@ export function formatBytes(bytes: number, decimals = 2) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`; return `${Number.parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
} }

View file

@ -1,14 +1,20 @@
{ {
"extends": "@vue/tsconfig/tsconfig.web.json", "extends": "@vue/tsconfig/tsconfig.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "**/*.d.ts", "node_modules/vite-plugin-pwa/client.d.ts"], "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "**/*.d.ts", "node_modules/vite-plugin-pwa/client.d.ts"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"lib": ["ES2021"], "lib": ["ES2022"],
"target": "es2022",
"module": "es2022",
"moduleResolution": "Node",
"composite": true, "composite": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },
"types": ["naive-ui/volar", "unplugin-icons/types/vue", "@intlify/unplugin-vue-i18n/messages"] "types": ["naive-ui/volar", "@intlify/unplugin-vue-i18n/messages", "unplugin-icons/types/vue"],
"esModuleInterop": true,
"jsx": "preserve",
"skipLibCheck": true
} }
} }

View file

@ -1,5 +1,5 @@
{ {
"extends": "@vue/tsconfig/tsconfig.node.json", "extends": "@tsconfig/node18/tsconfig.json",
"include": ["vite.config.*"], "include": ["vite.config.*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,

View file

@ -4,6 +4,6 @@
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"lib": [], "lib": [],
"types": ["node", "jsdom"] "types": ["node", "jsdom", "unplugin-icons/types/vue"]
} }
} }

View file

@ -25,7 +25,7 @@ export default defineConfig({
runtimeOnly: true, runtimeOnly: true,
compositionOnly: true, compositionOnly: true,
fullInstall: true, fullInstall: true,
include: [resolve(__dirname, 'locales/**'), resolve(__dirname, 'src/tools/*/locales/**')], include: [resolve(__dirname, 'locales/**')],
}), }),
AutoImport({ AutoImport({
imports: [ imports: [
@ -106,4 +106,7 @@ export default defineConfig({
test: { test: {
exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'], exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'],
}, },
build: {
target: 'esnext',
},
}); });