Compare commits

..

31 Commits

Author SHA1 Message Date
Corentin Thomasset
b3b6b7c46b chore(version): release 2023.05.14-77f2efc 2023-05-14 22:48:20 +02:00
Corentin Thomasset
141c12455e docs(changelog): update changelog for 2023.05.14-77f2efc 2023-05-14 22:48:20 +02:00
Corentin Thomasset
77f2efc0b9 refactor(ui): replaced some n-input with c-input-text 2023-05-14 22:30:23 +02:00
Corentin Thomasset
aad8d84e13 ui-lib(new-component): added text input component in the c-lib 2023-05-14 22:30:23 +02:00
Corentin Thomasset
401f13f7e3 ui-lib(button): size variants 2023-05-14 22:30:23 +02:00
Corentin THOMASSET
edae4c6915 chore(issues): updated new tool request issue template 2023-05-13 22:09:55 +02:00
Corentin Thomasset
a43c546e34 fix(phone-parser): use default country code 2023-05-07 13:25:33 +02:00
cgoIT
83a7b3bae9 feat(list-converter): a small converter who deals with column based data and do some stuff with it (#387)
* feat(list-converter): a small converter who deals with column based data and do some stuff with it

* Update src/tools/list-converter/index.ts

* Update src/tools/list-converter/index.ts

* Update src/tools/list-converter/index.ts

---------

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>

fix(list-format): fix e2e
2023-05-07 13:25:25 +02:00
Corentin Thomasset
ce3150c65d feat(new tool): phone parser and normalizer 2023-05-02 13:57:39 +02:00
Corentin Thomasset
3f6c8f0edd fix(home): prevent weird blue border on card 2023-05-01 13:44:30 +02:00
Corentin Thomasset
daf2cf0285 chore(version): release 2023.04.23-92bd835 2023-04-23 22:44:35 +02:00
Corentin Thomasset
b7aaea1b58 docs(changelog): update changelog for 2023.04.23-92bd835 2023-04-23 22:44:35 +02:00
Corentin Thomasset
92bd83536f feat(ui-lib): demo pages for c-lib components 2023-04-23 22:43:06 +02:00
Corentin Thomasset
e88c1d5f2c fix(ts): cleaned legacy typechecking warning 2023-04-23 17:11:04 +02:00
Corentin Thomasset
362f2fa280 feat(new-tool): diff of two json objects 2023-04-23 15:24:20 +02:00
Corentin Thomasset
61ece2387f refactor(ui-lib): prevent c-button to shrink 2023-04-20 21:03:20 +02:00
Corentin Thomasset
f080933d2a refactor(ui): replaced naive ui cards with custom ones 2023-04-20 20:57:38 +02:00
Corentin Thomasset
bb32513bd3 refactor(clean): removed unused lodash import 2023-04-19 22:58:07 +02:00
Corentin Thomasset
c311e3824d fix(mac-address-lookup): added copy handler on button click 2023-04-19 22:56:50 +02:00
Corentin Thomasset
74073f5038 refactor(clean): removed useless br tags 2023-04-19 22:50:02 +02:00
Corentin Thomasset
c45bce36f9 refactor(ui): getting ride of naive ui buttons 2023-04-19 22:33:22 +02:00
cgoIT
df989e24b3 feat(ipv4-range-expander): expands a given IPv4 start and end address to a valid IPv4 subnet (#366)
* feat(ipv4-range-expander): expands a given IPv4 start and end address to a valid IPv4 subnet

* feat(ipv4-range-expander): remove old component copyable-ip-like.vue

* feat(ipv4-range-expander): fix sonar findings

* feat(ipv4-range-expander): changes due to review

* feat(ipv4-range-expander): only show n-alert if both ipv4 addresses are valid
2023-04-19 20:30:45 +02:00
Corentin Thomasset
6d2202597c feat(date converter): auto focus main input 2023-04-19 13:07:24 +02:00
Corentin Thomasset
c68a1fd713 chore(version): release 2023.04.14-dbad773 2023-04-14 21:09:36 +02:00
Corentin Thomasset
46b1a07213 docs(changelog): update changelog for 2023.04.14-dbad773 2023-04-14 21:09:36 +02:00
Corentin Thomasset
dbad7730f9 chore(release): create a github release on new version 2023-04-14 21:08:43 +02:00
Corentin Thomasset
85cb0ffabd chore(version): reset CHANGELOG content to support new format 2023-04-14 18:34:23 +02:00
Corentin Thomasset
8355bd2ae4 feat(new-tool): http status codes 2023-04-14 09:04:49 +02:00
Corentin Thomasset
6fb4994603 refactor(uuid-generator): prevent NaN in quantity 2023-04-13 23:36:25 +02:00
Corentin Thomasset
7d7cc99866 chore(version): release v2023.04.13-dce9ff9 2023-04-13 01:09:14 +02:00
Corentin Thomasset
dce9ff91e2 feat(cd): git version tag pushed to docker 2023-04-13 01:04:48 +02:00
137 changed files with 4463 additions and 2172 deletions

View File

@@ -6,8 +6,8 @@ labels: new tool
assignees: CorentinTh
---
**Which tool is impacted?**
Example: the token generator
**What tool do you want?**
Example: a token generator
**Describe the solution you'd like**
A clear and concise description of what you want to happen.

View File

@@ -27,5 +27,8 @@ jobs:
- name: Run unit test
run: pnpm test
- name: Type check
run: pnpm typecheck
- name: Build the app
run: pnpm build

View File

@@ -46,3 +46,59 @@ jobs:
corentinth/it-tools:${{ env.RELEASE_VERSION }}
ghcr.io/corentinth/it-tools:latest
ghcr.io/corentinth/it-tools:${{ env.RELEASE_VERSION}}
github-release:
runs-on: ubuntu-latest
needs: docker-release
steps:
- name: Get release version
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
- run: corepack enable
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Build the app
run: pnpm build
- name: Zip the app
run: zip -r it-tools-${{ env.RELEASE_VERSION }}.zip dist/*
- name: Get changelog
id: changelog
run: |
EOF=$(openssl rand -hex 8)
echo "changelog<<$EOF" >> $GITHUB_OUTPUT
node ./scripts/getLatestChangelog.mjs >> $GITHUB_OUTPUT
echo "$EOF" >> $GITHUB_OUTPUT
- name: Create Release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: it-tools-${{ env.RELEASE_VERSION }}.zip
tag_name: v${{ env.RELEASE_VERSION }}
draft: true
prerelease: false
body: |
## Docker images
- Docker Hub
- `corentinth/it-tools:latest`
- `corentinth/it-tools:${{ env.RELEASE_VERSION }}`
- GitHub Container Registry
- `ghcr.io/corentinth/it-tools:latest`
- `ghcr.io/corentinth/it-tools:${{ env.RELEASE_VERSION}}`
## Changelog
${{ steps.changelog.outputs.changelog }}

View File

@@ -2,577 +2,93 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.19.0](https://github.com/CorentinTh/it-tools/compare/v2.18.0...v2.19.0) (2023-02-06)
## Version 2023.05.14-77f2efc
### Features
- **list-converter**: a small converter who deals with column based data and do some stuff with it (#387) (83a7b3b)
- **new tool**: phone parser and normalizer (ce3150c)
* **new-tool:** keycode info ([c934c4e](https://github.com/CorentinTh/it-tools/commit/c934c4e50ca1a129b80b786a5d9a7dbc33ad9ba3))
### Bug fixes
- **phone-parser**: use default country code (a43c546)
- **home**: prevent weird blue border on card (3f6c8f0)
## [2.18.0](https://github.com/CorentinTh/it-tools/compare/v2.17.0...v2.18.0) (2023-02-04)
### Refactoring
- **ui**: replaced some n-input with c-input-text (77f2efc)
### Chores
- **issues**: updated new tool request issue template (edae4c6)
### Ui-lib
- **new-component**: added text input component in the c-lib (aad8d84)
- **button**: size variants (401f13f)
## Version 2023.04.23-92bd835
### Features
- **ui-lib**: demo pages for c-lib components (92bd835)
- **new-tool**: diff of two json objects (362f2fa)
- **ipv4-range-expander**: expands a given IPv4 start and end address to a valid IPv4 subnet (#366) (df989e2)
- **date converter**: auto focus main input (6d22025)
* **new-tool:** json minify ([#265](https://github.com/CorentinTh/it-tools/issues/265)) ([f708f50](https://github.com/CorentinTh/it-tools/commit/f708f5091e2182fc88e7cf3e7d23b3d05edc04da))
### Bug fixes
- **ts**: cleaned legacy typechecking warning (e88c1d5)
- **mac-address-lookup**: added copy handler on button click (c311e38)
### Refactoring
- **ui-lib**: prevent c-button to shrink (61ece23)
- **ui**: replaced naive ui cards with custom ones (f080933)
- **clean**: removed unused lodash import (bb32513)
- **clean**: removed useless br tags (74073f5)
- **ui**: getting ride of naive ui buttons (c45bce3)
### Refactors
* **tools:** config in query params ([db817a2](https://github.com/CorentinTh/it-tools/commit/db817a2459e23bd096274a7f91815d613d5f7ff4))
## [2.17.0](https://github.com/CorentinTh/it-tools/compare/v2.16.0...v2.17.0) (2023-01-13)
## Version 2023.04.14-dbad773
### Features
- **new-tool**: http status codes (8355bd2)
* **new-tool:** jwt parser ([#262](https://github.com/CorentinTh/it-tools/issues/262)) ([acc7f0a](https://github.com/CorentinTh/it-tools/commit/acc7f0a586c64500c5f720e70cdbccf9bffe76d9))
* **new-tool:** temperature converter ([4607837](https://github.com/CorentinTh/it-tools/commit/4607837f9a398440e0098f2ba862e8d7422ce94f))
### Refactoring
- **uuid-generator**: prevent NaN in quantity (6fb4994)
### Chores
- **release**: create a github release on new version (dbad773)
- **version**: reset CHANGELOG content to support new format (85cb0ff)
### Refactors
* **jwt-parser:** simplified code ([f52f7a8](https://github.com/CorentinTh/it-tools/commit/f52f7a845c34ce7da57b11c17d261733be89554f))
## [2.16.0](https://github.com/CorentinTh/it-tools/compare/v2.15.0...v2.16.0) (2022-12-21)
## Version 2023.04.14-f9b77b7
### Features
- **new-tool**: http status codes (8355bd2)
* **search-bar:** use cmd + k to focus on mac ([bf88836](https://github.com/CorentinTh/it-tools/commit/bf88836dbe4037019e9545deaae1db06e5768cfb))
* **tool:** improved favorite tool management ([af075dc](https://github.com/CorentinTh/it-tools/commit/af075dccccec959a0863e6d11516206860bed91f))
* **tools:** added favorite tool handling ([4cd809b](https://github.com/CorentinTh/it-tools/commit/4cd809bd0c94836532f58a2ec6aa131694cce10d))
* **tracker:** added actions monitoring ([bfc2e24](https://github.com/CorentinTh/it-tools/commit/bfc2e24bbfc08f67ed9c9b1d93474029bc01dc8b))
### Refactoring
- **uuid-generator**: prevent NaN in quantity (6fb4994)
### Chores
- **release**: create a github release on new version (f9b77b7)
- **version**: reset CHANGELOG content to support new format (85cb0ff)
### Refactors
* **clean:** removed empty style tag ([cf723f1](https://github.com/CorentinTh/it-tools/commit/cf723f144ee865b6de7323d3be58eb7a9586fa56))
* **clean:** removed unused import ([4087285](https://github.com/CorentinTh/it-tools/commit/40872859a580a20bb838b79db2b3c88c00995e37))
* **menu:** improve support button ([679dd1c](https://github.com/CorentinTh/it-tools/commit/679dd1c1f6265227cc9db60c55d83f8eaf8f72b4))
* **tracker:** better tracker injection ([def60e7](https://github.com/CorentinTh/it-tools/commit/def60e7248003e74ed67e9ff116b438bab410a92))
## [2.15.0](https://github.com/CorentinTh/it-tools/compare/v2.14.1...v2.15.0) (2022-12-16)
## Version 2023.04.14-2f0d239
### Features
- **new-tool**: http status codes (8355bd2)
* **search-bar:** better search back result ([71e98e9](https://github.com/CorentinTh/it-tools/commit/71e98e93e5752cba934f67d679088524c4d3d2ad))
### Refactoring
- **uuid-generator**: prevent NaN in quantity (6fb4994)
### Chores
- **release**: create a github release on new version (2f0d239)
- **version**: reset CHANGELOG content to support new format (85cb0ff)
### Bug Fixes
* **integer-base-converter:** handle non-decimal char and better error message ([8476cf3](https://github.com/CorentinTh/it-tools/commit/8476cf319b7ebae87c7928592604a54833ac56ef))
* **tool-card:** correct text color on light mode for card description ([acf8bc1](https://github.com/CorentinTh/it-tools/commit/acf8bc11dbab85ab361edbe400ebbe5e52a11b89))
### Refactors
* **search-bar:** improved tool fuzzy search ([1b5d4e7](https://github.com/CorentinTh/it-tools/commit/1b5d4e72bdb222dd721a1e484c3e5d73bb62d2b1))
### [2.14.1](https://github.com/CorentinTh/it-tools/compare/v2.14.0...v2.14.1) (2022-11-23)
## [2.14.0](https://github.com/CorentinTh/it-tools/compare/v2.13.0...v2.14.0) (2022-11-23)
## Version 2023.04.14-474cae4
### Features
- **new-tool**: http status codes (8355bd2)
* **new-tool:** chmod calculator ([35b5187](https://github.com/CorentinTh/it-tools/commit/35b518711938c2bc88f35d104bb35d9956f0c267))
### Refactoring
- **uuid-generator**: prevent NaN in quantity (6fb4994)
## [2.13.0](https://github.com/CorentinTh/it-tools/compare/v2.11.0...v2.13.0) (2022-11-14)
### Chores
- **release**: create a github release on new version (474cae4)
- **version**: reset CHANGELOG content to support new format (85cb0ff)
## Version v2023.4.13-dce9ff9
### Features
* **config:** added tsx to allowed extension ([ea5e7a7](https://github.com/CorentinTh/it-tools/commit/ea5e7a7fc7df1a3a912193912a6ab80a8a36a256))
* **date-converter:** added mongodb objectID format ([4ef2588](https://github.com/CorentinTh/it-tools/commit/4ef25887b9d874b8789bf8dbabd8aab92b4b1b03))
* **new-tool:** added otp generator ([5f16885](https://github.com/CorentinTh/it-tools/commit/5f168859238e9c3a8b8bbaf6b550c4b9bd163e00))
* **new-tool:** mime type to extension converter ([7c9b8ac](https://github.com/CorentinTh/it-tools/commit/7c9b8ac178967151a4f921ac26e8c2fe8d23b886))
### Bug Fixes
* **ui:** remove icon transparency overlap ([35a3760](https://github.com/CorentinTh/it-tools/commit/35a376077116dd65b21f9a0786d2ecfc14db6051))
### Refactors
* **otp-generator:** changed url ([7f22995](https://github.com/CorentinTh/it-tools/commit/7f229959d64b7a932f32753e3838d87a819a9192))
* token generator can use a custom alphabet ([83da6b7](https://github.com/CorentinTh/it-tools/commit/83da6b7ee9db29e40faf288f9627257aa7124038))
* **ui:** change sponsor button location and caption ([5d8f46a](https://github.com/CorentinTh/it-tools/commit/5d8f46abf8d5a10cc4650efc87b12a9a6c537fe5))
* **useQRCode:** switched args to MaybeRef ([7de6c86](https://github.com/CorentinTh/it-tools/commit/7de6c86f9ead8d7315614cc508dfee4fed90e9c2))
## [2.12.0](https://github.com/CorentinTh/it-tools/compare/v2.10.3...v2.12.0) (2022-08-23)
### Features
* added colored share card ([ab7483b](https://github.com/CorentinTh/it-tools/commit/ab7483b5c2bd5aee1b8b609597c22b7b7b55606d))
* **config:** added tsx to allowed extension ([741a3c2](https://github.com/CorentinTh/it-tools/commit/741a3c25a915d8296987b23bda03f2b664d51ba6))
* **new-tool:** added otp generator ([cc6070a](https://github.com/CorentinTh/it-tools/commit/cc6070a16655bce9de90517bdda3bf6224ba139d))
* **new-tool:** meta tag generator ([164e32b](https://github.com/CorentinTh/it-tools/commit/164e32b4428b8dfaaddcefa06b767a8af94573a9))
### Bug Fixes
* **deps:** added missing optional deps ([4975590](https://github.com/CorentinTh/it-tools/commit/49755909bdaea9399e51b67fbd1a6d071acd3182))
* removed colored card border ([7c449f4](https://github.com/CorentinTh/it-tools/commit/7c449f4f2d491ce58726c5419a74dc295fa92905))
### Refactors
* **colored-card:** added transition on like hover ([da17696](https://github.com/CorentinTh/it-tools/commit/da17696293270005b1b7ec4aafc0df7496f602c7))
* **share:** updated share meta ([5222bd5](https://github.com/CorentinTh/it-tools/commit/5222bd5d04ad089ba4cbade399dada55e29dcde5))
* token generator can use a custom alphabet ([59ec629](https://github.com/CorentinTh/it-tools/commit/59ec6293b65526fe8dc527ac596d0e5af29b1e32))
* **useQRCode:** switched args to MaybeRef ([a89c9be](https://github.com/CorentinTh/it-tools/commit/a89c9bea42d598f4caba10800becd66a07bbcdc9))
## [2.11.0](https://github.com/CorentinTh/it-tools/compare/v2.10.3...v2.11.0) (2022-08-19)
### Features
* added colored share card ([ab7483b](https://github.com/CorentinTh/it-tools/commit/ab7483b5c2bd5aee1b8b609597c22b7b7b55606d))
* **new-tool:** meta tag generator ([164e32b](https://github.com/CorentinTh/it-tools/commit/164e32b4428b8dfaaddcefa06b767a8af94573a9))
### Bug Fixes
* **deps:** added missing optional deps ([4975590](https://github.com/CorentinTh/it-tools/commit/49755909bdaea9399e51b67fbd1a6d071acd3182))
* removed colored card border ([7c449f4](https://github.com/CorentinTh/it-tools/commit/7c449f4f2d491ce58726c5419a74dc295fa92905))
### Refactors
* **colored-card:** added transition on like hover ([da17696](https://github.com/CorentinTh/it-tools/commit/da17696293270005b1b7ec4aafc0df7496f602c7))
* **share:** updated share meta ([5222bd5](https://github.com/CorentinTh/it-tools/commit/5222bd5d04ad089ba4cbade399dada55e29dcde5))
### [2.10.3](https://github.com/CorentinTh/it-tools/compare/v2.10.2...v2.10.3) (2022-08-14)
### Refactors
* **share:** new share banner ([fcf4cfe](https://github.com/CorentinTh/it-tools/commit/fcf4cfe64d4c1c3814137c8ff23b83a1ca0d502d))
* **share:** updated twitter meta tags ([992f96b](https://github.com/CorentinTh/it-tools/commit/992f96b48a89e2793ccf75fb9e28b2ec7b7f62b6))
* **validation:** simplified validation management with helpers ([f54223f](https://github.com/CorentinTh/it-tools/commit/f54223fb0aaedbd101b5d3dc4176053533bb936a))
### [2.10.2](https://github.com/CorentinTh/it-tools/compare/v2.10.1...v2.10.2) (2022-08-04)
### Refactors
* **dry:** mutualised duplicated code with withDefaultOnError ([f6cd9b7](https://github.com/CorentinTh/it-tools/commit/f6cd9b76d38800e1a1f63d07152fc96cda562795))
* **home:** removed new tool first sort ([d30cd8a](https://github.com/CorentinTh/it-tools/commit/d30cd8a9abc3298c0a0b05f249e54318bb4537f2))
* **json-prettifier:** more permissive json parser ([8089c60](https://github.com/CorentinTh/it-tools/commit/8089c60000000c42c821c6586c128d3d2b248885))
* **lint:** added import rules ([208a373](https://github.com/CorentinTh/it-tools/commit/208a373fd08ac550778745eb6e4536bf02537da7))
### [2.10.1](https://github.com/CorentinTh/it-tools/compare/v2.10.0...v2.10.1) (2022-08-04)
### Bug Fixes
* **bip39-generator:** cleared an issue with the mnemonic validation ([ca7cb44](https://github.com/CorentinTh/it-tools/commit/ca7cb4438972ca09f28a6a40332ec94ceaa4aab4))
* **import:** removed auto added weird .js extension ([fda0b0c](https://github.com/CorentinTh/it-tools/commit/fda0b0ca25c1733542a4e797ac1a2150c546a660))
### Refactors
* **base64:** mutualized base64 functions into global utilities ([447bdf2](https://github.com/CorentinTh/it-tools/commit/447bdf2148098d70ba309e13d9b1e846b5064da1))
* **chronometer:** improved chronometer precision ([e48d60b](https://github.com/CorentinTh/it-tools/commit/e48d60b1ed19279f48441743f7ed69e8fd915011))
## [2.10.0](https://github.com/CorentinTh/it-tools/compare/v2.9.2...v2.10.0) (2022-08-03)
### Features
* **hash-text:** digest base selector ([#254](https://github.com/CorentinTh/it-tools/issues/254)) ([422b6eb](https://github.com/CorentinTh/it-tools/commit/422b6eb05a2fb5e7eec816a6bd2d37b53e4a6bdc))
* **new-tool:** an svg placeholder image generator ([129f74c](https://github.com/CorentinTh/it-tools/commit/129f74c371eaf09fdc3a19afb709cee40b7aaf7f))
* **new-tool:** hmac generator ([1bc6380](https://github.com/CorentinTh/it-tools/commit/1bc6380c6fdd7a9b500422a54bc508ab5557eb46))
### Bug Fixes
* **base64-to-string:** prevent validation error ([8a9e788](https://github.com/CorentinTh/it-tools/commit/8a9e7888dec41364c8c17b1234adcdc0616612b0))
* **bip39-generator:** typo in validation message ([7570ad9](https://github.com/CorentinTh/it-tools/commit/7570ad965602233f860b9e03177a5b9dacf1b034))
* **eta-calculator:** clamp inputs ([#249](https://github.com/CorentinTh/it-tools/issues/249)) ([531a25c](https://github.com/CorentinTh/it-tools/commit/531a25c1c4892835633ba5635c6ee48e1fbef31c))
* **wording:** removed spaces before ponctuation ([#252](https://github.com/CorentinTh/it-tools/issues/252)) ([5f03619](https://github.com/CorentinTh/it-tools/commit/5f03619ab44c0b35455c46698ec37d79e87555b5))
### Refactors
* **base64-to-file:** clean validation to convert base64 to file ([750a76b](https://github.com/CorentinTh/it-tools/commit/750a76b00fb79c0e9c2851c112141158ee0ffab1))
* **display:** mutualized code display ([0be33fb](https://github.com/CorentinTh/it-tools/commit/0be33fb337e8d82474922c0fdf9555aa328cd729))
* **lint:** externalization of prettier for simpler IDE support ([02c4963](https://github.com/CorentinTh/it-tools/commit/02c49635315661ca08deb0859c5ba33113368b9b))
* **validation:** simplified validation system ([77b5b0c](https://github.com/CorentinTh/it-tools/commit/77b5b0cab50a05dcb419ce87d74517d82e7cd2c0))
### [2.9.2](https://github.com/CorentinTh/it-tools/compare/v2.9.1...v2.9.2) (2022-07-28)
### Bug Fixes
* **base64-file:** fixed url slug ([412de23](https://github.com/CorentinTh/it-tools/commit/412de23796babbc080b0768a75029ff2ddf2acfc))
* **device-information:** handle of unknown values ([4f599b6](https://github.com/CorentinTh/it-tools/commit/4f599b699901a93444bcc67cbb3b3556a0561ae4))
* **device-information:** prevent unwanted y-truncature of text ([138149e](https://github.com/CorentinTh/it-tools/commit/138149e6f0be91255907a6083887898e5c68882e))
### Refactors
* **base64-file:** fixed typo ([1a22d55](https://github.com/CorentinTh/it-tools/commit/1a22d55b3c48f58b05b5a50de4fea260e781fbef))
### [2.9.1](https://github.com/CorentinTh/it-tools/compare/v2.9.0...v2.9.1) (2022-07-25)
### Refactors
* **base64:** split base64 text and file conversion in two tools + base64 to file ([e6953d1](https://github.com/CorentinTh/it-tools/commit/e6953d1b67b81a6d3c19973b706f29637c421f98))
## [2.9.0](https://github.com/CorentinTh/it-tools/compare/v2.8.0...v2.9.0) (2022-07-25)
### Features
* **new-tool:** added a basic auth generator ([bdee93a](https://github.com/CorentinTh/it-tools/commit/bdee93a9e45c6b46e7f75cdcbe1907f138722dca))
## [2.8.0](https://github.com/CorentinTh/it-tools/compare/v2.7.0...v2.8.0) (2022-07-24)
### Features
* **new-tool:** added an ETA calculator ([125a502](https://github.com/CorentinTh/it-tools/commit/125a50215a7abb9e0b59dbbc62aee49007b05ffe))
### Bug Fixes
* **sql-prettifier:** better responsiveness ([560fcf3](https://github.com/CorentinTh/it-tools/commit/560fcf3f783c66b9197e4a015420c43a729518bc))
### Refactors
* **json-prettify:** improved layout for the json prettifier ([328fda6](https://github.com/CorentinTh/it-tools/commit/328fda65b3490869328467c5e2d5f538c689d9b6))
* **sql-prettifier:** remove unused service files ([ba87097](https://github.com/CorentinTh/it-tools/commit/ba87097e3d834b6ea3212d28c2c33badb95f85e1))
## [2.7.0](https://github.com/CorentinTh/it-tools/compare/v2.6.0...v2.7.0) (2022-07-24)
### Features
* **new-tool:** added an SQL prettifier and formatter ([d1f95f5](https://github.com/CorentinTh/it-tools/commit/d1f95f5b34a4570f1033a5289f0bd009d1aefb0c))
### Bug Fixes
* **typo:** fix few typos ([6cd25a7](https://github.com/CorentinTh/it-tools/commit/6cd25a743e32fceeaec8c1f8b94927a9c5d901f1))
## [2.6.0](https://github.com/CorentinTh/it-tools/compare/v2.5.3...v2.6.0) (2022-07-23)
### Features
* **new-tool:** added chronometer ([130031c](https://github.com/CorentinTh/it-tools/commit/130031c2256f3d4d46948974b9de85ee6e92bf8b))
* **search:** focus the search bar using Ctrl+K ([ab53048](https://github.com/CorentinTh/it-tools/commit/ab53048d5f6fdca7d00edbb79dee1a5409e6b11e))
### Bug Fixes
* **deps:** run dependencie audit auto fix ([a16161c](https://github.com/CorentinTh/it-tools/commit/a16161cdb48c064882b9dc91ec3d091d286f5c63))
* **lint:** cleanned index.html ([c3a302b](https://github.com/CorentinTh/it-tools/commit/c3a302bc389a0e13aef4b14d5a9d3ec3a0d32729))
* **text-statistics:** empty text mean 0 words and 0 lines ([92ce419](https://github.com/CorentinTh/it-tools/commit/92ce419f45e110509ab202485a36bf175ce345da))
### Refactors
* added accessibility labels on icon buttons ([394d085](https://github.com/CorentinTh/it-tools/commit/394d085846d976219ea775c21cd7e77f0f72a12b))
* **import:** auto reordered imports ([2140842](https://github.com/CorentinTh/it-tools/commit/214084262cec7fb881fd397626356b080ea1a5cc))
### [2.5.3](https://github.com/CorentinTh/it-tools/compare/v2.5.2...v2.5.3) (2022-07-21)
### Bug Fixes
* updated license in README ([e371e8f](https://github.com/CorentinTh/it-tools/commit/e371e8fedfd68f3cf6ecd3fbc9e2da8849f7d5bd))
### [2.5.2](https://github.com/CorentinTh/it-tools/compare/v2.5.1...v2.5.2) (2022-07-21)
### [2.5.1](https://github.com/CorentinTh/it-tools/compare/v2.5.0...v2.5.1) (2022-06-01)
### Bug Fixes
* **lint:** missing dangling comma ([f05c8e1](https://github.com/CorentinTh/it-tools/commit/f05c8e1dc69275e529f4c8771ad55ba211e7fb5e))
* menu label key value was undefined ([f48cd05](https://github.com/CorentinTh/it-tools/commit/f48cd058cf3381f3bc92ea8fe37b565327707d1e))
* **title:** trully reactive tool title ([c2e1d59](https://github.com/CorentinTh/it-tools/commit/c2e1d59cb9d8dbb1bb072a46100192cb8c59f59b))
* tool sorting inconsistencies in home page ([5ab4dd3](https://github.com/CorentinTh/it-tools/commit/5ab4dd3d4a42c3609d72597c7ba91764170e6e96))
## [2.5.0](https://github.com/CorentinTh/it-tools/compare/v2.4.2...v2.5.0) (2022-06-01)
### Features
* **new-tool:** math evaluator ([433ba2a](https://github.com/CorentinTh/it-tools/commit/433ba2a3e5419eed0c96304b37693082224a1c73))
* **tools:** new badge for recently created tools ([11720e6](https://github.com/CorentinTh/it-tools/commit/11720e6cdefc1da4bdd638415813b609840f8462))
### Bug Fixes
* **config:** updated env values loading ([2f61c74](https://github.com/CorentinTh/it-tools/commit/2f61c745f57962cf3bb9e2c1db4a3176df042808))
### Refactors
* removed unused import ([8fb0e6a](https://github.com/CorentinTh/it-tools/commit/8fb0e6af9c3be708d3f1777a1661e1b38f197a3f))
* renammed Tool.ts to tool.ts ([ac89490](https://github.com/CorentinTh/it-tools/commit/ac89490794ee3c1c033859ffea31a962a13cc96d))
### [2.4.2](https://github.com/CorentinTh/it-tools/compare/v2.4.1...v2.4.2) (2022-06-01)
### Refactors
* **config:** added config management with figue ([6becdbb](https://github.com/CorentinTh/it-tools/commit/6becdbb42329e1bdecf158707e37ba9f13ba1d2c))
* **imports:** removed useless defineProps import ([5ce1262](https://github.com/CorentinTh/it-tools/commit/5ce1262fb44864b829dac09d5c0b9b68d522ceb7))
* set coerent head title for home page ([a46d125](https://github.com/CorentinTh/it-tools/commit/a46d125c19902c2f41f37c62c07bb7b548d9f6f0))
### [2.4.1](https://github.com/CorentinTh/it-tools/compare/v2.4.0...v2.4.1) (2022-05-15)
### Bug Fixes
* **seo:** wrong url in share metas ([a88e4a9](https://github.com/CorentinTh/it-tools/commit/a88e4a9289e7d8cc80190f60f2fe08fe2ba08ee6))
### Refactors
* **json-viewer:** add clear button ([048bc4a](https://github.com/CorentinTh/it-tools/commit/048bc4ae943509dea2946764efaa69f845b6c478))
* **seo:** changed title string ([d4ea393](https://github.com/CorentinTh/it-tools/commit/d4ea393c1df87ae958a06ed66a11e36b081282d4))
## [2.4.0](https://github.com/CorentinTh/it-tools/compare/v2.3.2...v2.4.0) (2022-05-14)
### Features
* catch throw on validation ([a60f64f](https://github.com/CorentinTh/it-tools/commit/a60f64f74417f811204121f97c16cdb4754afc3b))
* **hash-text:** compute all hashes at the same time ([#242](https://github.com/CorentinTh/it-tools/issues/242)) ([e9cc499](https://github.com/CorentinTh/it-tools/commit/e9cc499ed87ba926086323223c7eca4f6658b3f0))
* **new-tool:** json viewer ([d356b14](https://github.com/CorentinTh/it-tools/commit/d356b1488fc640a4f5b65d62e0f2f368f5941996))
* **seo:** added cannonical meta ([34bc6a5](https://github.com/CorentinTh/it-tools/commit/34bc6a57a7bab98ff2a630d02034c342084e0af9))
### Bug Fixes
* **lint:** missing new lines ([3cfc5f8](https://github.com/CorentinTh/it-tools/commit/3cfc5f8bc27b66e6fbb6054f3c909818083ebc37))
* update recommended extension ids ([#244](https://github.com/CorentinTh/it-tools/issues/244)) ([1d7032d](https://github.com/CorentinTh/it-tools/commit/1d7032d0268220f594de6d837a303fc1e63cbd9f))
### Documentation
* added producthunt banners ([4c4da16](https://github.com/CorentinTh/it-tools/commit/4c4da16970e1dbb13705d8b6c020cd40cd2b5e0d))
### Refactors
* **base-layout:** renammed one letter variable ([383d975](https://github.com/CorentinTh/it-tools/commit/383d97569580c4f31448c07cb97e3778bc97a8af))
* **date-converter:** mutualised and dry-ed code ([d2c767f](https://github.com/CorentinTh/it-tools/commit/d2c767f0922e9b93172c3167226ad3db5499b9f6))
* **seo:** changed title string ([c3b6132](https://github.com/CorentinTh/it-tools/commit/c3b6132c261bd5952bafb1ff1e576eb13d2d0a7d))
* updated description ([b89db3c](https://github.com/CorentinTh/it-tools/commit/b89db3c8d0de601fecbd2f9f79492dff1b461bd8))
### [2.3.2](https://github.com/CorentinTh/it-tools/compare/v2.3.1...v2.3.2) (2022-05-09)
### Bug Fixes
* **base-converter:** responsive input ([0b0cbd5](https://github.com/CorentinTh/it-tools/commit/0b0cbd55c3809ded2eedfa0b2238bc950b01516a))
* **base64-converter:** async onUpload callback ([84cf1bb](https://github.com/CorentinTh/it-tools/commit/84cf1bb9645c5ae31579098df59471f7d99f6f0c))
* **typo:** misspelings ([9755e51](https://github.com/CorentinTh/it-tools/commit/9755e51fe216e5e25c56417152e70cb5bce26b11))
### Refactors
* **responsive:** row layout for multicards on big screens ([e21230b](https://github.com/CorentinTh/it-tools/commit/e21230bbd9550ba3315607b021a60a4f9f9e1b61))
### [2.3.1](https://github.com/CorentinTh/it-tools/compare/v2.3.0...v2.3.1) (2022-04-24)
### Refactors
* changed twitter account handler ([608ec3a](https://github.com/CorentinTh/it-tools/commit/608ec3a81db6583c8a2bf126b3868afd043c6981))
## [2.3.0](https://github.com/CorentinTh/it-tools/compare/v2.2.0...v2.3.0) (2022-04-22)
### Features
* **new-tool:** html entities escape/unescape ([8e29a97](https://github.com/CorentinTh/it-tools/commit/8e29a97404ea0aa9b9b576656358c8c276b6f992))
### Bug Fixes
* **head:** added titles for non-tool pages ([0a15892](https://github.com/CorentinTh/it-tools/commit/0a15892dde9852ff158a8fcb72d0ad6bae8bad02))
* **sider:** default collapsed value ([b22aa94](https://github.com/CorentinTh/it-tools/commit/b22aa941f52009118d4d3cc98277cc4c402a4c77))
* **sider:** missing href for link in footer ([c4dabcc](https://github.com/CorentinTh/it-tools/commit/c4dabccdaeac9d03163ac2588599b000e4e74562))
* **style:** hard width for group labels ([ebf6695](https://github.com/CorentinTh/it-tools/commit/ebf6695d2533db6f37b24dc7d338f422c551c8cb))
* **url-parser:** cleaned weird margins on dark mode ([005ebfb](https://github.com/CorentinTh/it-tools/commit/005ebfba318ece1a9c04aefb737baed5d7aafb91))
### Refactors
* **lint:** linter auto fix ([086d31e](https://github.com/CorentinTh/it-tools/commit/086d31eab5b3b1a927803eab5e650585f61abe19))
* removed useless ref and value ([b12cbe4](https://github.com/CorentinTh/it-tools/commit/b12cbe412407389186a58e4ceaa94f5b441c11ea))
### [2.2.1](https://github.com/CorentinTh/it-tools/compare/v2.2.0...v2.2.1) (2022-04-21)
### Bug Fixes
* **head:** added titles for non-tool pages ([0a15892](https://github.com/CorentinTh/it-tools/commit/0a15892dde9852ff158a8fcb72d0ad6bae8bad02))
* **sider:** missing href for link in footer ([c4dabcc](https://github.com/CorentinTh/it-tools/commit/c4dabccdaeac9d03163ac2588599b000e4e74562))
* **style:** hard width for group labels ([ebf6695](https://github.com/CorentinTh/it-tools/commit/ebf6695d2533db6f37b24dc7d338f422c551c8cb))
* **url-parser:** cleaned weird margins on dark mode ([005ebfb](https://github.com/CorentinTh/it-tools/commit/005ebfba318ece1a9c04aefb737baed5d7aafb91))
## [2.2.0](https://github.com/CorentinTh/it-tools/compare/v2.1.0...v2.2.0) (2022-04-18)
### Features
* **new-tool:** url parser ([2b38d6f](https://github.com/CorentinTh/it-tools/commit/2b38d6f81e34845f896b858513e35209cba29f98))
### Bug Fixes
* **sider-footer:** fixed commit sha url ([ed9046d](https://github.com/CorentinTh/it-tools/commit/ed9046d3e1f5a7dc01c722ed139a2ae477a2d48f))
## [2.1.0](https://github.com/CorentinTh/it-tools/compare/v2.0.2...v2.1.0) (2022-04-18)
### Features
* **new-tool:** bcrypt ([6d5856f](https://github.com/CorentinTh/it-tools/commit/6d5856fa93d1ffbf71856c75adc24ad87dc4b49b))
* **new-tool:** device information ([277bd5f](https://github.com/CorentinTh/it-tools/commit/277bd5f0da359fd54c5164b376007d182a9fabde))
### Refactors
* **menu:** removed burger menu icon tooltip ([09abffb](https://github.com/CorentinTh/it-tools/commit/09abffbcf9b09cb5adc34f8754b019d0c8b60854))
### [2.0.2](https://github.com/CorentinTh/it-tools/compare/v2.0.1...v2.0.2) (2022-04-18)
### Bug Fixes
* **git-memo:** pre scroll on overflow ([4fc303e](https://github.com/CorentinTh/it-tools/commit/4fc303e5e3f0bef9201cc002963e244a5d3be7b5))
* **menu:** menu auto closed on mobile ([71f79a5](https://github.com/CorentinTh/it-tools/commit/71f79a5bbfe0dd5451a435c0a55e8b77ee7d3848))
* **qr-code:** responsive layout ([cbf0b3d](https://github.com/CorentinTh/it-tools/commit/cbf0b3d6995e47d371a8fbcfccd65ba304fb08dc))
### Refactors
* **crontab:** list instead of table on small screen ([6b11de2](https://github.com/CorentinTh/it-tools/commit/6b11de258a8039fe7729130ede35d47592be7cbe))
* removed empty sources ([a14cac6](https://github.com/CorentinTh/it-tools/commit/a14cac6d5c5967a47ca76a1d1a420115114c3bbf))
* throw an error object instead of string ([4112fa5](https://github.com/CorentinTh/it-tools/commit/4112fa532e3d4be190d52bf3b11e0d4c3625a402))
### [2.0.1](https://github.com/CorentinTh/it-tools/compare/v2.0.0...v2.0.1) (2022-04-16)
### Features
* **config:** added vercel.json ([2e046ad](https://github.com/CorentinTh/it-tools/commit/2e046ad09fed4a55bbf4449e3683a4150839c461))
### Bug Fixes
* remove duplicate property ([d066319](https://github.com/CorentinTh/it-tools/commit/d066319b45dee35df0212c7ff407013bd7449ae3))
* **style:** url encode/decode layout ([34480b4](https://github.com/CorentinTh/it-tools/commit/34480b4e25ffc33536b03a0ba711c480219ad553))
### Documentation
* updated description ([70a3df0](https://github.com/CorentinTh/it-tools/commit/70a3df044ea86ac35c1839ac5ab624f694fdd845))
### Refactors
* clean imports ([724e142](https://github.com/CorentinTh/it-tools/commit/724e142222202798ea3df7d0fb34da1e7a5216a1))
* lint fix ([a58ae24](https://github.com/CorentinTh/it-tools/commit/a58ae24d9409728ac12fb780f2c64643087de5be))
* ref name ([5828085](https://github.com/CorentinTh/it-tools/commit/582808597c6aadf0feb48f6aae0a29b839e0dd54))
## 2.0.0 (2022-04-16)
### Features
* **a11y:** aria-label on icon button ([5f50275](https://github.com/CorentinTh/it-tools/commit/5f502755d69ab21a78d9256db8a1c64f1ab82c2a))
* added commit short sha ([668625c](https://github.com/CorentinTh/it-tools/commit/668625c6dab6e8b98f363df6c0aa3bf00a3afaa4))
* added plausible tracker ([0808920](https://github.com/CorentinTh/it-tools/commit/0808920951b55c938537f33353a37ece96b04084))
* added twitter link ([d126abc](https://github.com/CorentinTh/it-tools/commit/d126abc7b12a9fce778fe9883e44dca581509778))
* footer in sider ([3f03850](https://github.com/CorentinTh/it-tools/commit/3f038503dd705ba3a5562a1e8f85a3b0e7d0be5b))
* **layout:** menu category ([9c9be9e](https://github.com/CorentinTh/it-tools/commit/9c9be9e2e2e2c856d1af1df9d9d37a64460cd82b))
* mobile friendly menu ([1e67fa6](https://github.com/CorentinTh/it-tools/commit/1e67fa6e0bede8c055d9e4cb9bf7f97423bc9bdf))
* **navbar:** added github link ([d4e226e](https://github.com/CorentinTh/it-tools/commit/d4e226e09face78da794fa7e676eef85d05dde75))
* **nav:** navigation tooltips ([b892f50](https://github.com/CorentinTh/it-tools/commit/b892f50cd633d42e6261be208bd077d92d336afb))
* **page:** added 404 page ([3db4f91](https://github.com/CorentinTh/it-tools/commit/3db4f91c27a2ab37bb23d8feb77b6dffa9a92977))
* **page:** home page layout ([57fd14a](https://github.com/CorentinTh/it-tools/commit/57fd14a199a253f49f3c53810490e5d31512b261))
* persistent theme selection fallback to prefered theme ([40e9af0](https://github.com/CorentinTh/it-tools/commit/40e9af06cf28b7348152f8ec3898fa2b27ec0b21))
* **router:** added legacy routes redirections ([dbce46b](https://github.com/CorentinTh/it-tools/commit/dbce46b470b0187a395cdd350a023641c6319582))
* search-bar ([e8594de](https://github.com/CorentinTh/it-tools/commit/e8594de7b45102b8bc1cfb82d0839e3722d9c4c2))
* **search:** round and clearable searchbar ([b112f5f](https://github.com/CorentinTh/it-tools/commit/b112f5f226c6b03151bbeb4fc607e449c444e667))
* **seo:** added robots.txt and humans.txt ([cd9a3bc](https://github.com/CorentinTh/it-tools/commit/cd9a3bc9b10cf7363301e9a0d0b17f38ea640e0c))
* **seo:** added title + description ([5f74037](https://github.com/CorentinTh/it-tools/commit/5f74037105c5e8efc5bdad2261597458cfcf26d3))
* **seo:** pwa and icons ([b7193e8](https://github.com/CorentinTh/it-tools/commit/b7193e838ba83d0548211cff922e107a1f11f90f))
* **share:** social image ([39746e0](https://github.com/CorentinTh/it-tools/commit/39746e07c53c22ac132ad2aaf25dd71bb6458cde))
* **style:** dark mode ([3e92b7f](https://github.com/CorentinTh/it-tools/commit/3e92b7f1e04a709df231fce22801b55619e8faab))
* **style:** theme overrides ([d542688](https://github.com/CorentinTh/it-tools/commit/d542688664cc9c675d1d26f4278a25f1b9e3f28d))
* **tool:** add lch in color converter ([b5243c4](https://github.com/CorentinTh/it-tools/commit/b5243c43638f37a2d727b015bba61fab0d1b9fe9))
* **tool:** added token generator ([40dec52](https://github.com/CorentinTh/it-tools/commit/40dec52c8467fd27eb8f3857ed72746ebaa4f509))
* **tool:** base converter ([034c686](https://github.com/CorentinTh/it-tools/commit/034c686896d0443ea587cd152535b2227234c011))
* **tool:** base64 string converter ([203b6a9](https://github.com/CorentinTh/it-tools/commit/203b6a9d73dcb30182b130de59920534e18b76b4))
* **tool:** bip39-generator ([d55329f](https://github.com/CorentinTh/it-tools/commit/d55329f3abc3d3f8ad48def7d7f63b44cd768e27))
* **tool:** bip39-generator ([765c010](https://github.com/CorentinTh/it-tools/commit/765c010700c07b2809daef0e7c694ac265ce9ddc))
* **tool:** case converter ([7a7372d](https://github.com/CorentinTh/it-tools/commit/7a7372df191abc7ecd3fee7234d4de7aaaba03f6))
* **tool:** color converter ([4e50b7a](https://github.com/CorentinTh/it-tools/commit/4e50b7a973e950819a52c127db2a754838cbbf8e))
* **tool:** crontab generator ([358ff45](https://github.com/CorentinTh/it-tools/commit/358ff45ae1d9822b8a7c342515f668d25b7128b5))
* **tool:** date-time converter ([2d9cb20](https://github.com/CorentinTh/it-tools/commit/2d9cb209b377326f4bf62067db7d5ad0c7eb7bde))
* **tool:** encryption ([888ab2c](https://github.com/CorentinTh/it-tools/commit/888ab2cf378597e2880b6dd6a013f3bc192f2b1a))
* **tool:** git memo ([5cd9997](https://github.com/CorentinTh/it-tools/commit/5cd9997a845f6d5f82d3ae74d3ec12603224517d))
* **tool:** lorem ipsum generator ([5dcb2ed](https://github.com/CorentinTh/it-tools/commit/5dcb2ed95c318ea1c4134da207c844672d0fbbd8))
* **tool:** qr-code generator ([5582d75](https://github.com/CorentinTh/it-tools/commit/5582d75927b560d9259929c787c0809634d1f8ae))
* **tool:** random port generator ([7c540f1](https://github.com/CorentinTh/it-tools/commit/7c540f1208da749c3932aab8f2c392048c4546ae))
* **tool:** roman-arabic numbers converter ([655019c](https://github.com/CorentinTh/it-tools/commit/655019cf23babcec2a2f1e03cac87744e3139304))
* **tool:** text hash ([0f3b744](https://github.com/CorentinTh/it-tools/commit/0f3b7445ad1f945d9b364476147bf824ac309a6c))
* **tool:** text statistics ([0a7c325](https://github.com/CorentinTh/it-tools/commit/0a7c3252e36a4769eedaaec4524b4ee2ae2b19c7))
* **tool:** url encode/decode ([afac566](https://github.com/CorentinTh/it-tools/commit/afac5664c802c8480fe2c457bcfb7f5e26829cdf))
* **tool:** uuid v4 generator ([3ae6114](https://github.com/CorentinTh/it-tools/commit/3ae61147a94791987e9e326b19063579976d8dc0))
* **ux:** copyable input ([1859a9a](https://github.com/CorentinTh/it-tools/commit/1859a9a174010789dcd7ecefb2451e1de7b60b4c))
### Bug Fixes
* **hash-text:** added missing toString() ([4ca5fce](https://github.com/CorentinTh/it-tools/commit/4ca5fce911c3312d56bca1ffba863b2f37841c9e))
* **hash-text:** correct copy message ([bab92ef](https://github.com/CorentinTh/it-tools/commit/bab92ef84f66372df40ce385c2949518ed158427))
* removed global define ([889d594](https://github.com/CorentinTh/it-tools/commit/889d59499212a449ee460c68c480648e337a7ecb))
* **style:** working dark mode persistence ([3ae8728](https://github.com/CorentinTh/it-tools/commit/3ae872847b00d65e4e2e629775d479a3333450f1))
* **validation:** proper rules ([11d8110](https://github.com/CorentinTh/it-tools/commit/11d8110226e22e30ae16d297628c1d252a93be9e))
### Refactors
* better icon ([0af7d81](https://github.com/CorentinTh/it-tools/commit/0af7d81abd987aa5d1b0321c25a65131d978e929))
* **clean:** removed extra console.log ([82606f6](https://github.com/CorentinTh/it-tools/commit/82606f6a477fce2041ab33adc7e95bcba4343e2b))
* embeded sider scrollbar ([f872972](https://github.com/CorentinTh/it-tools/commit/f872972e69aeb4fde4c17f0c122ca3fd4aa1c56c))
* icon sizes ([9bb7fc4](https://github.com/CorentinTh/it-tools/commit/9bb7fc47aa70bdc5083d0883f1496fac63f812ea))
* menu option key ([390ef93](https://github.com/CorentinTh/it-tools/commit/390ef93232dc1b448022a0c09d36367adad9d221))
* **page:** removed unused import ([f70fce6](https://github.com/CorentinTh/it-tools/commit/f70fce65e20989eb19b0f0976e756a43edf02e9d))
* removed theme editor ([8559fbd](https://github.com/CorentinTh/it-tools/commit/8559fbd7744fe82b7702a5c0eb77a8d627c5a73d))
* removed unused files ([c1e7669](https://github.com/CorentinTh/it-tools/commit/c1e76695e4a16b8312ab6031a1bdfb6368946677))
* removed unused files ([8d9f924](https://github.com/CorentinTh/it-tools/commit/8d9f92417744a5fbd9b4108e851005f23de18b53))
* **style:** cleaner layout ([1d09a01](https://github.com/CorentinTh/it-tools/commit/1d09a01bb25088493cc9b7f2cb7f8a8aa69ac9e9))
* **style:** improve style for tool-card ([65a6896](https://github.com/CorentinTh/it-tools/commit/65a6896563d16f30420424e274bd306e3e9182c8))
* **style:** label width ([fd4426d](https://github.com/CorentinTh/it-tools/commit/fd4426d246ada553528759f761c8192df85c0d44))
* **style:** menu item height ([8951e87](https://github.com/CorentinTh/it-tools/commit/8951e87c143fda74be32bae5b28e009556d7086e))
* **style:** menu scrollbar ([483cf66](https://github.com/CorentinTh/it-tools/commit/483cf66db992169d361487c8461938810793b978))
* **style:** port display ([2632f24](https://github.com/CorentinTh/it-tools/commit/2632f24cc89af7dd12f7a0c1a8b58983a1bb78d8))
* **style:** removed extra br ([b44539c](https://github.com/CorentinTh/it-tools/commit/b44539c1820defbaaa6dfe83a76c72982a641971))
* **style:** replaced scss style block to less ([655d9d2](https://github.com/CorentinTh/it-tools/commit/655d9d22e3136bdf1dee29310ab04cf38596bdc8))
* **style:** responsive layout ([2df3f53](https://github.com/CorentinTh/it-tools/commit/2df3f53b78bbe419763fd359788a4b0b5710e4b7))
* **style:** updated linter config ([6b58ec5](https://github.com/CorentinTh/it-tools/commit/6b58ec554a0de91139f16d67cec42536d093d5fb))
### Documentation
* added new tool creation procedure ([8177883](https://github.com/CorentinTh/it-tools/commit/81778834e6a79725c42eae1772935682ce7580c6))
* updated readme ([1134e0b](https://github.com/CorentinTh/it-tools/commit/1134e0b822edbc25ce9ff83007bf5d331a1becbd))
_Diff not available_

72
auto-imports.d.ts vendored
View File

@@ -19,7 +19,9 @@ declare global {
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
@@ -39,9 +41,6 @@ declare global {
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const logicAnd: typeof import('@vueuse/core')['logicAnd']
const logicNot: typeof import('@vueuse/core')['logicNot']
const logicOr: typeof import('@vueuse/core')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
@@ -92,8 +91,9 @@ declare global {
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRef: typeof import('@vueuse/core')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('@vueuse/core')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
@@ -104,6 +104,19 @@ declare global {
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
@@ -114,8 +127,8 @@ declare global {
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClamp: typeof import('@vueuse/core')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
@@ -189,12 +202,18 @@ declare global {
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
@@ -208,14 +227,17 @@ declare global {
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
@@ -227,6 +249,8 @@ declare global {
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
@@ -247,8 +271,10 @@ declare global {
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
@@ -282,7 +308,9 @@ declare module 'vue' {
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
@@ -302,9 +330,6 @@ declare module 'vue' {
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly logicAnd: UnwrapRef<typeof import('@vueuse/core')['logicAnd']>
readonly logicNot: UnwrapRef<typeof import('@vueuse/core')['logicNot']>
readonly logicOr: UnwrapRef<typeof import('@vueuse/core')['logicOr']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
@@ -355,8 +380,9 @@ declare module 'vue' {
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRef: UnwrapRef<typeof import('@vueuse/core')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('@vueuse/core')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@@ -367,6 +393,19 @@ declare module 'vue' {
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
@@ -377,8 +416,8 @@ declare module 'vue' {
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClamp: UnwrapRef<typeof import('@vueuse/core')['useClamp']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
@@ -452,12 +491,18 @@ declare module 'vue' {
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
@@ -471,14 +516,17 @@ declare module 'vue' {
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
@@ -490,6 +538,8 @@ declare module 'vue' {
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
@@ -510,8 +560,10 @@ declare module 'vue' {
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>

93
components.d.ts vendored
View File

@@ -9,13 +9,81 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
'404.page': typeof import('./src/pages/404.page.vue')['default']
About: typeof import('./src/pages/About.vue')['default']
App: typeof import('./src/App.vue')['default']
'Base.layout': typeof import('./src/layouts/base.layout.vue')['default']
Base64FileConverter: typeof import('./src/tools/base64-file-converter/base64-file-converter.vue')['default']
Base64StringConverter: typeof import('./src/tools/base64-string-converter/base64-string-converter.vue')['default']
BasicAuthGenerator: typeof import('./src/tools/basic-auth-generator/basic-auth-generator.vue')['default']
Bcrypt: typeof import('./src/tools/bcrypt/bcrypt.vue')['default']
BenchmarkBuilder: typeof import('./src/tools/benchmark-builder/benchmark-builder.vue')['default']
Bip39Generator: typeof import('./src/tools/bip39-generator/bip39-generator.vue')['default']
CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default']
CButton: typeof import('./src/ui/c-button/c-button.vue')['default']
'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default']
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default']
'CInputText.theme': typeof import('./src/ui/c-input-text/c-input-text.theme.vue')['default']
CLink: typeof import('./src/ui/c-link/c-link.vue')['default']
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
copy: typeof import('./src/ui/c-input-text/c-input-text copy.vue')['default']
CopyableIpLike: typeof import('./src/tools/ipv4-subnet-calculator/copyable-ip-like.vue')['default']
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default']
'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default']
DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default']
DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default']
DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default']
DockerRunToDockerComposeConverter: typeof import('./src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue')['default']
DynamicValues: typeof import('./src/tools/benchmark-builder/dynamic-values.vue')['default']
Editor: typeof import('./src/tools/html-wysiwyg-editor/editor/editor.vue')['default']
Encryption: typeof import('./src/tools/encryption/encryption.vue')['default']
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
GitMemo: typeof import('./src/tools/git-memo/git-memo.md')['default']
HashText: typeof import('./src/tools/hash-text/hash-text.vue')['default']
HmacGenerator: typeof import('./src/tools/hmac-generator/hmac-generator.vue')['default']
'Home.page': typeof import('./src/pages/Home.page.vue')['default']
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
IconMdiClose: typeof import('~icons/mdi/close')['default']
IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
IconMdiEye: typeof import('~icons/mdi/eye')['default']
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
Ipv4RangeExpander: typeof import('./src/tools/ipv4-range-expander/ipv4-range-expander.vue')['default']
Ipv4SubnetCalculator: typeof import('./src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue')['default']
Ipv6UlaGenerator: typeof import('./src/tools/ipv6-ula-generator/ipv6-ula-generator.vue')['default']
JsonDiff: typeof import('./src/tools/json-diff/json-diff.vue')['default']
JsonMinify: typeof import('./src/tools/json-minify/json-minify.vue')['default']
JsonToYaml: typeof import('./src/tools/json-to-yaml-converter/json-to-yaml.vue')['default']
JsonViewer: typeof import('./src/tools/json-viewer/json-viewer.vue')['default']
JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default']
ListConverter: typeof import('./src/tools/list-converter/list-converter.vue')['default']
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default']
MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default']
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NAutoComplete: typeof import('naive-ui')['NAutoComplete']
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
@@ -32,6 +100,7 @@ declare module '@vue/runtime-core' {
NEllipsis: typeof import('naive-ui')['NEllipsis']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1']
@@ -49,7 +118,6 @@ declare module '@vue/runtime-core' {
NP: typeof import('naive-ui')['NP']
NPageHeader: typeof import('naive-ui')['NPageHeader']
NProgress: typeof import('naive-ui')['NProgress']
NResult: typeof import('naive-ui')['NResult']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSlider: typeof import('naive-ui')['NSlider']
@@ -62,11 +130,34 @@ declare module '@vue/runtime-core' {
NTooltip: typeof import('naive-ui')['NTooltip']
NUpload: typeof import('naive-ui')['NUpload']
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']
RomanNumeralConverter: typeof import('./src/tools/roman-numeral-converter/roman-numeral-converter.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default']
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
SearchBarItem: typeof import('./src/components/SearchBarItem.vue')['default']
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default']
TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default']
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default']
TextToNatoAlphabet: typeof import('./src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue')['default']
TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default']
'TokenGenerator.tool': typeof import('./src/tools/token-generator/token-generator.tool.vue')['default']
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default']
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']
UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default']
UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "it-tools",
"version": "2023.4.13-023cc75",
"version": "2023.5.14-77f2efc",
"description": "Collection of handy online tools for developers, with great UX. ",
"keywords": [
"productivity",
@@ -30,7 +30,7 @@
"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",
"script:create-new-tool": "node scripts/create-tool.mjs",
"release": "standard-version"
"release": "node ./scripts/release.mjs"
},
"dependencies": {
"@it-tools/bip39": "^0.0.4",
@@ -48,6 +48,7 @@
"change-case": "^4.1.2",
"colord": "^2.9.3",
"composerize-ts": "^0.6.2",
"country-code-lookup": "^0.0.23",
"cron-validator": "^1.3.1",
"cronstrue": "^2.26.0",
"crypto-js": "^4.1.1",
@@ -57,6 +58,7 @@
"highlight.js": "^11.7.0",
"json5": "^2.2.3",
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.28",
"lodash": "^4.17.21",
"mathjs": "^10.6.4",
"mime-types": "^2.1.35",
@@ -77,6 +79,7 @@
"yaml": "^2.2.1"
},
"devDependencies": {
"@iconify-json/mdi": "^1.1.50",
"@playwright/test": "^1.32.3",
"@rushstack/eslint-patch": "^1.2.0",
"@types/bcryptjs": "^2.4.2",
@@ -96,11 +99,14 @@
"@unocss/eslint-config": "^0.50.8",
"@vitejs/plugin-vue": "^2.3.4",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/compiler-sfc": "^3.2.47",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^10.0.0",
"@vue/runtime-core": "^3.2.47",
"@vue/test-utils": "^2.3.2",
"@vue/tsconfig": "^0.1.3",
"c8": "^7.13.0",
"consola": "^3.0.2",
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-typescript": "^3.5.5",
@@ -109,11 +115,11 @@
"jsdom": "^19.0.0",
"less": "^4.1.3",
"prettier": "^2.8.7",
"standard-version": "^9.5.0",
"start-server-and-test": "^1.15.4",
"typescript": "~4.5.5",
"unocss": "^0.50.8",
"unplugin-auto-import": "^0.15.2",
"unplugin-icons": "^0.16.1",
"unplugin-vue-components": "^0.24.1",
"vite": "^2.9.15",
"vite-plugin-md": "^0.12.4",
@@ -121,6 +127,7 @@
"vite-svg-loader": "^3.6.0",
"vitest": "^0.13.1",
"vue-tsc": "^0.31.4",
"workbox-window": "^6.5.4"
"workbox-window": "^6.5.4",
"zx": "^7.2.1"
}
}

995
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,9 +29,9 @@ createToolFile(
`${toolName}.vue`,
`
<template>
<n-card>
<div>
Lorem ipsum
</n-card>
</div>
</template>
<script setup lang="ts">

View File

@@ -0,0 +1,6 @@
import { readFile } from 'fs/promises';
const changelogContent = await readFile('./CHANGELOG.md', 'utf-8');
const [, lastChangelog] = changelogContent.split(/^## .*$/gm);
console.log(lastChangelog.trim());

57
scripts/release.mjs Normal file
View File

@@ -0,0 +1,57 @@
import { $, argv } from 'zx';
import { consola } from 'consola';
import { rawCommitsToMarkdown } from './shared/commits.mjs';
import { addToChangelog } from './shared/changelog.mjs';
$.verbose = false;
const isDryRun = argv['dry-run'] ?? false;
const now = new Date();
const currentShortSha = (await $`git rev-parse --short HEAD`).stdout.trim();
const calver = now.toISOString().slice(0, 10).replace(/-/g, '.');
const version = `${calver}-${currentShortSha}`;
const { stdout: rawCommits } = await $`git log --pretty=oneline $(git describe --tags --abbrev=0)..HEAD`;
const markdown = rawCommitsToMarkdown({ rawCommits });
consola.info(`Changelog: \n\n${markdown}\n\n`);
if (isDryRun) {
consola.info(`[dry-run] Not creating version nor tag`);
consola.info('Aborting');
process.exit(0);
}
const shouldContinue = await consola.prompt(
'This script will create a new version and tag, and update the changelog. Continue?',
{
type: 'confirm',
},
);
if (!shouldContinue) {
consola.info('Aborting');
process.exit(0);
}
consola.info('Updating changelog');
await addToChangelog({ changelog: markdown, version });
consola.success('Changelog updated');
try {
consola.info('Committing changelog changes');
await $`git add CHANGELOG.md`;
await $`git commit -m "docs(changelog): update changelog for ${version}"`;
consola.success('Changelog changes committed');
consola.info('Creating version and tag');
await $`npm version ${version} -m "chore(version): release ${version}"`;
consola.info('Npm version released with tag');
} catch (error) {
consola.error(error);
consola.info('Aborting');
process.exit(1);
}

View File

@@ -0,0 +1,15 @@
import { readFile, writeFile } from 'fs/promises';
export { addToChangelog };
async function addToChangelog({ changelog, version, changelogPath = './CHANGELOG.md' }) {
const changelogContent = await readFile(changelogPath, 'utf-8');
const versionTitle = `## Version ${version}`;
if (changelogContent.includes(versionTitle)) {
throw new Error(`Version ${version} already exists in the changelog`);
}
const newChangeLogContent = changelogContent.replace('## ', `${versionTitle}\n\n${changelog}\n\n## `);
await writeFile(changelogPath, newChangeLogContent, 'utf-8');
}

View File

@@ -0,0 +1,54 @@
import _ from 'lodash';
export { rawCommitsToMarkdown };
const commitScopesToHumanReadable = {
build: 'Build system',
chore: 'Chores',
ci: 'Continuous integration',
docs: 'Documentation',
feat: 'Features',
fix: 'Bug fixes',
infra: 'Infrastucture',
perf: 'Performance',
refactor: 'Refactoring',
test: 'Tests',
};
const commitTypesOrder = ['feat', 'fix', 'perf', 'refactor', 'test', 'build', 'ci', 'chore', 'other'];
const getCommitTypeSortIndex = (type) =>
commitTypesOrder.includes(type) ? commitTypesOrder.indexOf(type) : commitTypesOrder.length;
function parseCommitLine(commit) {
const [sha, ...splittedRawMessage] = commit.trim().split(' ');
const rawMessage = splittedRawMessage.join(' ');
const { type, scope, subject } = /^(?<type>.*?)(\((?<scope>.*)\))?: ?(?<subject>.+)$/.exec(rawMessage)?.groups ?? {};
return {
sha: sha.slice(0, 7),
type: type ?? 'other',
scope,
subject: subject ?? rawMessage,
};
}
function commitSectionsToMarkdown({ type, commits }) {
return [
`### ${commitScopesToHumanReadable[type] ?? _.capitalize(type)}`,
...commits.map(({ sha, scope, subject }) => ['-', scope ? `**${scope}**:` : '', subject, `(${sha})`].join(' ')),
].join('\n');
}
function rawCommitsToMarkdown({ rawCommits }) {
return _.chain(rawCommits)
.trim()
.split('\n')
.map(parseCommitLine)
.groupBy('type')
.map((commits, type) => ({ type, commits }))
.sortBy(({ type }) => getCommitTypeSortIndex(type))
.map(commitSectionsToMarkdown)
.join('\n\n')
.value();
}

View File

@@ -1,5 +1,5 @@
<template>
<n-card class="colored-card">
<c-card class="colored-card">
<n-space justify="space-between" align="center">
<n-icon class="icon" size="40" :component="icon" />
</n-space>
@@ -12,7 +12,7 @@
<slot />
</n-ellipsis>
</div>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -1,11 +1,15 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<n-button circle quaternary :type="buttonType" :style="{ opacity: isFavorite ? 1 : 0.2 }" @click="toggleFavorite">
<template #icon>
<n-icon :component="FavoriteFilled" />
</template>
</n-button>
<c-button
variant="text"
circle
:type="buttonType"
:style="{ opacity: isFavorite ? 1 : 0.2 }"
@click="toggleFavorite"
>
<n-icon :component="FavoriteFilled" />
</c-button>
</template>
{{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }}
</n-tooltip>

View File

@@ -1,5 +1,5 @@
<template>
<n-form-item :label="inputLabel" v-bind="validationAttrs">
<n-form-item :label="inputLabel" v-bind="validationAttrs as any">
<n-input
ref="inputElement"
v-model:value="input"
@@ -10,7 +10,7 @@
autocorrect="off"
autocapitalize="off"
spellcheck="false"
:input-props="{ 'data-test-id': 'input' }"
:input-props="{ 'data-test-id': 'input' } as any"
/>
</n-form-item>
<n-form-item :label="outputLabel">

View File

@@ -1,21 +1,20 @@
<template>
<n-input v-model:value="value">
<c-input-text v-model:value="value">
<template #suffix>
<n-tooltip trigger="hover">
<template #trigger>
<n-button quaternary circle @click="onCopyClicked">
<n-icon :component="ContentCopyFilled" />
</n-button>
<c-button circle variant="text" size="small" @click="onCopyClicked">
<icon-mdi-content-copy />
</c-button>
</template>
{{ tooltipText }}
</n-tooltip>
</template>
</n-input>
</c-input-text>
</template>
<script setup lang="ts">
import { useVModel, useClipboard } from '@vueuse/core';
import { ContentCopyFilled } from '@vicons/material';
import { ref } from 'vue';
const props = defineProps<{ value: string }>();
@@ -35,9 +34,3 @@ function onCopyClicked() {
}, 2000);
}
</script>
<style scoped>
::v-deep(.n-input-wrapper) {
padding-right: 5px;
}
</style>

View File

@@ -1,56 +1,50 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<n-button
size="large"
<c-button
circle
quaternary
tag="a"
variant="text"
href="https://github.com/CorentinTh/it-tools"
rel="noopener"
target="_blank"
rel="noopener noreferrer"
aria-label="IT-Tools' GitHub repository"
>
<n-icon size="25" :component="BrandGithub" />
</n-button>
</c-button>
</template>
Github repository
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<n-button
size="large"
<c-button
circle
quaternary
tag="a"
variant="text"
href="https://twitter.com/ittoolsdottech"
rel="noopener"
target="_blank"
aria-label="IT Tools' Twitter account"
>
<n-icon size="25" :component="BrandTwitter" />
</n-button>
</c-button>
</template>
IT Tools' Twitter account
</n-tooltip>
<router-link to="/about" #="{ navigate, href }" custom>
<n-tooltip trigger="hover">
<template #trigger>
<n-button tag="a" :href="href" circle quaternary size="large" aria-label="About" @click="navigate">
<n-icon size="25" :component="InfoCircle" />
</n-button>
</template>
About
</n-tooltip>
</router-link>
<n-tooltip trigger="hover">
<template #trigger>
<n-button size="large" circle quaternary aria-label="Toggle dark/light mode" @click="isDarkTheme = !isDarkTheme">
<c-button circle variant="text" to="/about" aria-label="About">
<n-icon size="25" :component="InfoCircle" />
</c-button>
</template>
About
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<c-button circle variant="text" aria-label="Toggle dark/light mode" @click="toggleDarkTheme">
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
<n-icon v-else size="25" :component="Moon" />
</n-button>
</c-button>
</template>
<span v-if="isDarkTheme">Light mode</span>
<span v-else>Dark mode</span>
@@ -59,11 +53,20 @@
<script setup lang="ts">
import { useStyleStore } from '@/stores/style.store';
import { useThemeStore } from '@/ui/theme/theme.store';
import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler';
import { toRefs } from 'vue';
const styleStore = useStyleStore();
const { isDarkTheme } = toRefs(styleStore);
const themeStore = useThemeStore();
function toggleDarkTheme() {
isDarkTheme.value = !isDarkTheme.value;
themeStore.toggleTheme();
}
</script>
<style lang="less" scoped>

View File

@@ -81,7 +81,7 @@ function onFocus() {
<n-auto-complete
v-model:value="queryString"
:options="options"
:on-select="(value) => onSelect(String(value))"
:on-select="(value: string | number) => onSelect(String(value))"
:render-label="renderOption"
:default-value="'aa'"
:get-show="() => displayDropDown"

View File

@@ -1,7 +1,7 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<span class="ip" @click="handleClick">{{ ip }}</span>
<span class="value" @click="handleClick">{{ value }}</span>
</template>
{{ tooltipText }}
</n-tooltip>
@@ -11,13 +11,13 @@
import { useClipboard } from '@vueuse/core';
import { ref, toRefs } from 'vue';
const props = withDefaults(defineProps<{ ip?: string }>(), { ip: '' });
const { ip } = toRefs(props);
const props = withDefaults(defineProps<{ value?: string }>(), { value: '' });
const { value } = toRefs(props);
const initialText = 'Copy to clipboard';
const tooltipText = ref(initialText);
const { copy } = useClipboard({ source: ip });
const { copy } = useClipboard({ source: value });
function handleClick() {
copy();
@@ -28,8 +28,8 @@ function handleClick() {
</script>
<style scoped lang="less">
.ip {
font-family: monospace;
.value {
cursor: pointer;
font-family: monospace;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div style="overflow-x: hidden; width: 100%">
<n-card class="result-card">
<c-card class="result-card">
<n-scrollbar
x-scrollable
trigger="none"
@@ -13,16 +13,16 @@
<n-tooltip v-if="value" trigger="hover">
<template #trigger>
<div class="copy-button" :class="[copyPlacement]">
<n-button circle secondary size="large" @click="onCopyClicked">
<c-button circle important:h-10 important:w-10 @click="onCopyClicked">
<n-icon size="22" :component="Copy" />
</n-button>
</c-button>
</div>
</template>
<span>{{ tooltipText }}</span>
</n-tooltip>
</n-card>
</c-card>
<n-space v-if="copyPlacement === 'outside'" justify="center" mt-4>
<n-button secondary @click="onCopyClicked"> {{ tooltipText }} </n-button>
<c-button @click="onCopyClicked"> {{ tooltipText }} </c-button>
</n-space>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<router-link :to="tool.path">
<n-card class="tool-card">
<c-card class="tool-card">
<n-space justify="space-between" align="center">
<n-icon class="icon" size="40" :component="tool.icon" />
<n-space align="center">
@@ -29,7 +29,7 @@
<br />&nbsp;
</n-ellipsis>
</div>
</n-card>
</c-card>
</router-link>
</template>
@@ -37,11 +37,14 @@
import type { Tool } from '@/tools/tools.types';
import { useThemeVars } from 'naive-ui';
import { toRefs } from 'vue';
import { useAppTheme } from '@/ui/theme/themes';
import FavoriteButton from './FavoriteButton.vue';
const props = defineProps<{ tool: Tool & { category: string } }>();
const { tool } = toRefs(props);
const theme = useThemeVars();
const appTheme = useAppTheme();
</script>
<style lang="less" scoped>
@@ -50,8 +53,12 @@ a {
}
.tool-card {
transition: border-color ease 0.5s;
border-width: 2px !important;
color: transparent;
&:hover {
border-color: var(--n-color-target);
border-color: v-bind('appTheme.primary.colorHover');
}
.icon {

View File

@@ -1,9 +1,8 @@
import { useClipboard } from '@vueuse/core';
import { useClipboard, type MaybeRef, get } from '@vueuse/core';
import { useMessage } from 'naive-ui';
import type { Ref } from 'vue';
export function useCopy({ source, text = 'Copied to the clipboard' }: { source: Ref; text?: string }) {
const { copy } = useClipboard({ source });
export function useCopy({ source, text = 'Copied to the clipboard' }: { source: MaybeRef<unknown>; text?: string }) {
const { copy } = useClipboard({ source: computed(() => String(get(source))) });
const message = useMessage();
return {

View File

@@ -26,7 +26,7 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue:
return computed<T>({
get() {
return transformer.fromQuery(proxy.value) as T;
return transformer.fromQuery(proxy.value) as unknown as T;
},
set(value) {
proxy.value = transformer.toQuery(value as never);

View File

@@ -1,3 +1,4 @@
import { get, type MaybeRef } from '@vueuse/core';
import _ from 'lodash';
import { reactive, watch, type Ref } from 'vue';
@@ -31,7 +32,7 @@ export function useValidation<T>({
watch: watchRefs = [],
}: {
source: Ref<T>;
rules: UseValidationRule<T>[];
rules: MaybeRef<UseValidationRule<T>[]>;
watch?: Ref<unknown>[];
}) {
const state = reactive<{
@@ -55,7 +56,7 @@ export function useValidation<T>({
state.message = '';
state.status = undefined;
for (const rule of rules) {
for (const rule of get(rules)) {
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
state.message = rule.message;
state.status = 'error';

View File

@@ -23,9 +23,9 @@ export const config = figue({
env: {
doc: 'Application current env',
format: 'enum',
values: ['production', 'development', 'test'],
values: ['production', 'development', 'preview', 'test'],
default: 'development',
env: 'MODE',
env: 'VITE_VERCEL_ENV',
},
},
plausible: {

View File

@@ -53,38 +53,25 @@ const tools = computed<ToolCategory[]>(() => [
<div>
IT-Tools
<n-button
text
tag="a"
target="_blank"
rel="noopener"
type="primary"
depth="3"
:href="`https://github.com/CorentinTh/it-tools/tree/v${version}`"
>
<c-link target="_blank" rel="noopener" :href="`https://github.com/CorentinTh/it-tools/tree/v${version}`">
v{{ version }}
</n-button>
</c-link>
<template v-if="commitSha && commitSha.length > 0">
-
<n-button
text
tag="a"
<c-link
target="_blank"
rel="noopener"
type="primary"
depth="3"
:href="`https://github.com/CorentinTh/it-tools/tree/${commitSha}`"
>
{{ commitSha }}
</n-button>
</c-link>
</template>
</div>
<div>
© {{ new Date().getFullYear() }}
<n-button text tag="a" target="_blank" rel="noopener" type="primary" href="https://github.com/CorentinTh">
Corentin Thomasset
</n-button>
<c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh"> Corentin Thomasset </c-link>
</div>
</div>
</div>
@@ -92,34 +79,24 @@ const tools = computed<ToolCategory[]>(() => [
<template #content>
<div class="navigation">
<n-button
<c-button
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
circle
quaternary
variant="text"
aria-label="Toggle menu"
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
>
<n-icon size="25" :component="Menu2" />
</n-button>
</c-button>
<router-link to="/" #="{ navigate, href }" custom>
<n-tooltip trigger="hover">
<template #trigger>
<n-button
tag="a"
:href="href"
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
circle
quaternary
aria-label="Home"
@click="navigate"
>
<n-icon size="25" :component="Home2" />
</n-button>
</template>
Home
</n-tooltip>
</router-link>
<n-tooltip trigger="hover">
<template #trigger>
<c-button to="/" circle variant="text" aria-label="Home">
<n-icon size="25" :component="Home2" />
</c-button>
</template>
Home
</n-tooltip>
<search-bar />
@@ -127,10 +104,8 @@ const tools = computed<ToolCategory[]>(() => [
<n-tooltip trigger="hover">
<template #trigger>
<n-button
<c-button
round
type="primary"
tag="a"
href="https://www.buymeacoffee.com/cthmsst"
rel="noopener"
target="_blank"
@@ -140,7 +115,7 @@ const tools = computed<ToolCategory[]>(() => [
>
Buy me a coffee
<n-icon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
</n-button>
</c-button>
</template>
Support IT Tools development !
</n-tooltip>
@@ -165,8 +140,8 @@ const tools = computed<ToolCategory[]>(() => [
.support-button {
background: rgb(37, 99, 108);
background: linear-gradient(48deg, rgba(37, 99, 108, 1) 0%, rgba(59, 149, 111, 1) 60%, rgba(20, 160, 88, 1) 100%);
color: #fff;
transition: all ease 0.2s;
color: #fff !important;
transition: padding ease 0.2s !important;
&:hover {
color: #fff;

View File

@@ -13,8 +13,6 @@ useHead({ title: 'Page not found - IT Tools' });
<n-text mt-4 block depth="3">Sorry, this page does not seem to exist</n-text>
<n-text mb-8 block depth="3">Maybe the cache is doing tricky things, try force-refreshing?</n-text>
<router-link to="/" #="{ navigate, href }" custom>
<n-button tag="a" :href="href" secondary @click="navigate"> Back home </n-button>
</router-link>
<c-button to="/"> Back home </c-button>
</div>
</template>

View File

@@ -11,25 +11,21 @@ const { tracker } = useTracker();
<n-h1>About</n-h1>
<n-p>
This wonderful website, made with by
<n-button text tag="a" href="https://github.com/CorentinTh" target="_blank" rel="noopener" type="primary">
Corentin Thomasset </n-button
>, aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to
share it to people you think may find it useful too and don't forget to pin it in your shortcut bar !
<c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener"> Corentin Thomasset </c-link>,
aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to share
it to people you think may find it useful too and don't forget to pin it in your shortcut bar !
</n-p>
<n-p>
IT Tools is open-source (under the MIT license) and free, and will always be, but it cost me money to host and
renew the domain name, if you want to support my work, and encourage me to add more tools, please consider
supporting by
<n-button
type="primary"
tag="a"
text
<c-link
href="https://www.buymeacoffee.com/cthmsst"
rel="noopener"
target="_blank"
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
>
sponsoring me </n-button
sponsoring me </c-link
>.
</n-p>
@@ -37,16 +33,9 @@ const { tracker } = useTracker();
<n-p>
IT Tools is made in Vue JS (vue 3) with the the naive-ui component library and is hosted and continuously deployed
by Vercel. Third party open-source libraries are used in some tools, you may find the complete list in the
<n-button
type="primary"
tag="a"
text
href="https://github.com/CorentinTh/it-tools/blob/main/package.json"
rel="noopener"
target="_blank"
>
<c-link href="https://github.com/CorentinTh/it-tools/blob/main/package.json" rel="noopener" target="_blank">
package.json
</n-button>
</c-link>
file of the repository.
</n-p>
@@ -54,30 +43,24 @@ const { tracker } = useTracker();
<n-p>
If you need a tool that is currently not present here, and you think can be relevant, you are welcome to submit a
feature request in the
<n-button
type="primary"
tag="a"
text
<c-link
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=enhancement&template=feature_request.md&title=%5BFEAT%5D%20My%20feature"
rel="noopener"
target="_blank"
>
issues section
</n-button>
</c-link>
in the GitHub repository.
</n-p>
<n-p>
And if you found a bug, or something broken that doesn't work as expected, please fill a bug report in the
<n-button
type="primary"
tag="a"
text
<c-link
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=bug&template=bug_report.md&title=%5BBUG%5D%20My%20bug"
rel="noopener"
target="_blank"
>
issues section
</n-button>
</c-link>
in the GitHub repository.
</n-p>
</div>

View File

@@ -4,6 +4,7 @@ import HomePage from './pages/Home.page.vue';
import NotFound from './pages/404.page.vue';
import { tools } from './tools';
import { config } from './config';
import { routes as demoRoutes } from './ui/demo/demo.routes';
const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
path,
@@ -32,6 +33,7 @@ const router = createRouter({
},
...toolsRoutes,
...toolsRedirectRoutes,
...(config.app.env === 'development' ? demoRoutes : []),
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
],
});

View File

@@ -1,5 +1,5 @@
<template>
<n-card title="Base64 to file">
<c-card title="Base64 to file">
<n-form-item
:feedback="base64InputValidation.message"
:validation-status="base64InputValidation.status"
@@ -8,13 +8,13 @@
<n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" />
</n-form-item>
<n-space justify="center">
<n-button :disabled="base64Input === '' || !base64InputValidation.isValid" secondary @click="downloadFile()">
<c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()">
Download file
</n-button>
</c-button>
</n-space>
</n-card>
</c-card>
<n-card title="File to base64">
<c-card title="File to base64">
<n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image">
<n-upload-dragger>
<div mb-2>
@@ -26,9 +26,9 @@
<n-input :value="fileBase64" type="textarea" readonly placeholder="File in base64 will be here" />
<n-space justify="center">
<n-button secondary @click="copyFileBase64()"> Copy </n-button>
<c-button @click="copyFileBase64()"> Copy </c-button>
</n-space>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -1,5 +1,5 @@
<template>
<n-card title="String to base64">
<c-card title="String to base64">
<n-form-item label="String to encode">
<n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." rows="5" />
</n-form-item>
@@ -15,12 +15,12 @@
</n-form-item>
<n-space justify="center">
<n-button secondary @click="copyTextBase64()"> Copy base64 </n-button>
<c-button @click="copyTextBase64()"> Copy base64 </c-button>
</n-space>
</n-card>
</c-card>
<n-card title="Base64 to string">
<n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs">
<c-card title="Base64 to string">
<n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs as any">
<n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" />
</n-form-item>
@@ -29,9 +29,9 @@
</n-form-item>
<n-space justify="center">
<n-button secondary @click="copyText()"> Copy decoded string </n-button>
<c-button @click="copyText()"> Copy decoded string </c-button>
</n-space>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -1,29 +1,25 @@
<template>
<div>
<n-form-item label="Username">
<n-input v-model:value="username" placeholder="Your username..." clearable />
</n-form-item>
<n-form-item label="Password">
<n-input
v-model:value="password"
placeholder="Your password..."
type="password"
show-password-on="click"
clearable
/>
</n-form-item>
<c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 />
<c-input-text
v-model:value="password"
label="Password"
placeholder="Your password..."
clearable
raw-text
mb-2
type="password"
/>
<br />
<n-card>
<c-card>
<n-statistic label="Authorization header:" class="header">
<n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none">
{{ header }}
</n-scrollbar>
</n-statistic>
</n-card>
<br />
<n-space justify="center">
<n-button secondary @click="copy">Copy header</n-button>
</c-card>
<n-space justify="center" mt-5>
<c-button @click="copy">Copy header</c-button>
</n-space>
</div>
</template>

View File

@@ -1,48 +1,32 @@
<template>
<n-card title="Hash">
<n-form label-width="120">
<n-form-item label="Your string: " label-placement="left">
<n-input
v-model:value="input"
placeholder="Your string to bcrypt..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</n-form-item>
<n-form-item label="Salt count: " label-placement="left">
<n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
</n-form-item>
<n-input :value="hashed" readonly style="text-align: center" />
</n-form>
<br />
<n-space justify="center">
<n-button secondary @click="copy"> Copy hash </n-button>
</n-space>
</n-card>
<c-card title="Hash">
<c-input-text
v-model:value="input"
placeholder="Your string to bcrypt..."
raw-text
label="Your string: "
label-position="left"
label-width="120px"
mb-2
/>
<n-form-item label="Salt count: " label-placement="left" label-width="120">
<n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
</n-form-item>
<n-card title="Compare string with hash">
<c-input-text :value="hashed" readonly text-center />
<n-space justify="center" mt-5>
<c-button @click="copy"> Copy hash </c-button>
</n-space>
</c-card>
<c-card title="Compare string with hash">
<n-form label-width="120">
<n-form-item label="Your string: " label-placement="left">
<n-input
v-model:value="compareString"
placeholder="Your string to compare..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
<c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text />
</n-form-item>
<n-form-item label="Your hash: " label-placement="left">
<n-input
v-model:value="compareHash"
placeholder="Your hahs to compare..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
<c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text />
</n-form-item>
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
<div class="compare-result" :class="{ positive: compareMatch }">
@@ -50,7 +34,7 @@
</div>
</n-form-item>
</n-form>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -1,53 +1,52 @@
<template>
<n-scrollbar style="flex: 1" x-scrollable>
<n-space :wrap="false" style="flex: 1" justify="center" :size="0">
<n-space :wrap="false" style="flex: 1" justify="center" :size="12" mb-5>
<div v-for="(suite, index) of suites" :key="index">
<n-card style="width: 292px; margin: 0 8px 5px">
<n-form-item label="Suite name:" :show-feedback="false" label-placement="left">
<n-input v-model:value="suite.title" placeholder="Suite name..." />
</n-form-item>
<c-card style="width: 294px">
<c-input-text
v-model:value="suite.title"
label-position="left"
label="Suite name"
placeholder="Suite name..."
clearable
/>
<n-divider></n-divider>
<n-form-item label="Suite values" :show-feedback="false">
<dynamic-values v-model:values="suite.data" />
</n-form-item>
</n-card>
</c-card>
<n-space justify="center">
<n-button v-if="suites.length > 1" quaternary @click="suites.splice(index, 1)">
<template #icon>
<n-icon :component="Trash" depth="3" />
</template>
<c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)">
<n-icon :component="Trash" depth="3" mr-2 size="18" />
Delete suite
</n-button>
<n-button quaternary @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })">
<template #icon>
<n-icon :component="Plus" depth="3" />
</template>
</c-button>
<c-button
variant="text"
@click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })"
>
<n-icon :component="Plus" depth="3" mr-2 size="18" />
Add suite
</n-button>
</c-button>
</n-space>
</div>
</n-space>
<br />
</n-scrollbar>
<div style="flex: 0 0 100%">
<div style="max-width: 600px; margin: 0 auto">
<n-space justify="center">
<n-form-item label="Unit:" label-placement="left">
<n-input v-model:value="unit" placeholder="Unit (eg: ms)" />
</n-form-item>
<c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 />
<n-button
tertiary
<c-button
@click="
suites = [
{ title: 'Suite 1', data: [] },
{ title: 'Suite 2', data: [] },
]
"
>Reset suites</n-button
>Reset suites</c-button
>
</n-space>
@@ -71,10 +70,9 @@
</tr>
</tbody>
</n-table>
<br />
<n-space justify="center">
<n-button tertiary @click="copyAsMarkdown">Copy as markdown table</n-button>
<n-button tertiary @click="copyAsBulletList">Copy as bullet list</n-button>
<n-space justify="center" mt-5>
<c-button @click="copyAsMarkdown">Copy as markdown table</c-button>
<c-button @click="copyAsBulletList">Copy as bullet list</c-button>
</n-space>
</div>
</div>

View File

@@ -11,22 +11,18 @@
/>
<n-tooltip>
<template #trigger>
<n-button circle quaternary @click="values.splice(index, 1)">
<template #icon>
<n-icon :component="Trash" depth="3" />
</template>
</n-button>
<c-button circle variant="text" @click="values.splice(index, 1)">
<n-icon :component="Trash" depth="3" size="18" />
</c-button>
</template>
Delete value
</n-tooltip>
</n-space>
<n-button tertiary @click="addValue">
<template #icon>
<n-icon :component="Plus" />
</template>
<c-button @click="addValue">
<n-icon :component="Plus" depth="3" mr-2 size="18" />
Add a measure
</n-button>
</c-button>
</div>
</template>

View File

@@ -1,59 +1,50 @@
<template>
<div>
<n-card>
<n-grid cols="3" x-gap="12">
<n-gi span="1">
<n-form-item label="Language:">
<n-select
v-model:value="language"
:options="Object.keys(languages).map((label) => ({ label, value: label }))"
/>
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item
label="Entropy (seed):"
:feedback="entropyValidation.message"
:validation-status="entropyValidation.status"
>
<n-input-group>
<n-input v-model:value="entropy" placeholder="Your string..." />
<n-button @click="refreshEntropy">
<n-icon size="22">
<Refresh />
</n-icon>
</n-button>
<n-button @click="copyEntropy">
<n-icon size="22">
<Copy />
</n-icon>
</n-button>
</n-input-group>
</n-form-item>
</n-gi>
</n-grid>
<n-form-item
label="Passphrase (mnemonic):"
:feedback="mnemonicValidation.message"
:validation-status="mnemonicValidation.status"
>
<n-input-group>
<n-input
v-model:value="passphrase"
style="text-align: center; flex: 1"
placeholder="Your mnemonic..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
<n-grid cols="3" x-gap="12">
<n-gi span="1">
<n-form-item label="Language:">
<n-select
v-model:value="language"
:options="Object.keys(languages).map((label) => ({ label, value: label }))"
/>
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item
label="Entropy (seed):"
:feedback="entropyValidation.message"
:validation-status="entropyValidation.status"
>
<n-input-group>
<c-input-text v-model:value="entropy" placeholder="Your string..." />
<n-button @click="copyPassphrase">
<n-icon size="22" :component="Copy" />
</n-button>
</n-input-group>
</n-form-item>
</n-card>
<c-button @click="refreshEntropy">
<n-icon size="22">
<Refresh />
</n-icon>
</c-button>
<c-button @click="copyEntropy">
<n-icon size="22">
<Copy />
</n-icon>
</c-button>
</n-input-group>
</n-form-item>
</n-gi>
</n-grid>
<n-form-item
label="Passphrase (mnemonic):"
:feedback="mnemonicValidation.message"
:validation-status="mnemonicValidation.status"
>
<n-input-group>
<c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text />
<c-button @click="copyPassphrase">
<n-icon size="22" :component="Copy" />
</c-button>
</n-input-group>
</n-form-item>
</div>
</template>

View File

@@ -1,9 +1,15 @@
<template>
<n-card>
<c-card>
<n-form label-width="120" label-placement="left" :show-feedback="false">
<n-form-item label="Your string:">
<n-input v-model:value="input" />
</n-form-item>
<c-input-text
v-model:value="input"
label="Your string"
label-position="left"
label-width="120px"
label-align="right"
placeholder="Your string..."
raw-text
/>
<n-divider />
@@ -41,7 +47,7 @@
<input-copyable :value="snakeCase(input, baseConfig)" />
</n-form-item>
</n-form>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -1,14 +1,13 @@
<template>
<div>
<n-card>
<c-card>
<div class="duration">{{ formatMs(counter) }}</div>
</n-card>
<br />
<n-space justify="center">
<n-button v-if="!isRunning" secondary type="primary" @click="resume">Start</n-button>
<n-button v-else secondary type="warning" @click="pause">Stop</n-button>
</c-card>
<n-space justify="center" mt-5>
<c-button v-if="!isRunning" type="primary" @click="resume">Start</c-button>
<c-button v-else type="warning" @click="pause">Stop</c-button>
<n-button secondary @click="counter = 0">Reset</n-button>
<c-button @click="counter = 0">Reset</c-button>
</n-space>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<n-card>
<c-card>
<n-form label-width="100" label-placement="left">
<n-form-item label="color picker:">
<n-color-picker
@@ -9,28 +9,28 @@
/>
</n-form-item>
<n-form-item label="color name:">
<input-copyable v-model:value="name" :on-input="(v: string) => onInputUpdated(v, 'name')" />
<input-copyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" />
</n-form-item>
<n-form-item label="hex:">
<input-copyable v-model:value="hex" :on-input="(v: string) => onInputUpdated(v, 'hex')" />
<input-copyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" />
</n-form-item>
<n-form-item label="rgb:">
<input-copyable v-model:value="rgb" :on-input="(v: string) => onInputUpdated(v, 'rgb')" />
<input-copyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" />
</n-form-item>
<n-form-item label="hsl:">
<input-copyable v-model:value="hsl" :on-input="(v: string) => onInputUpdated(v, 'hsl')" />
<input-copyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" />
</n-form-item>
<n-form-item label="hwb:">
<input-copyable v-model:value="hwb" :on-input="(v: string) => onInputUpdated(v, 'hwb')" />
<input-copyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" />
</n-form-item>
<n-form-item label="lch:">
<input-copyable v-model:value="lch" :on-input="(v: string) => onInputUpdated(v, 'lch')" />
<input-copyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" />
</n-form-item>
<n-form-item label="cmyk:">
<input-copyable v-model:value="cmyk" :on-input="(v: string) => onInputUpdated(v, 'cmyk')" />
<input-copyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" />
</n-form-item>
</n-form>
</n-card>
</c-card>
</template>
<script setup lang="ts">
@@ -54,15 +54,19 @@ const cmyk = ref('');
const lch = ref('');
function onInputUpdated(value: string, omit: string) {
const color = colord(value);
try {
const color = colord(value);
if (omit !== 'name') name.value = color.toName({ closest: true }) ?? '';
if (omit !== 'hex') hex.value = color.toHex();
if (omit !== 'rgb') rgb.value = color.toRgbString();
if (omit !== 'hsl') hsl.value = color.toHslString();
if (omit !== 'hwb') hwb.value = color.toHwbString();
if (omit !== 'cmyk') cmyk.value = color.toCmykString();
if (omit !== 'lch') lch.value = color.toLchString();
if (omit !== 'name') name.value = color.toName({ closest: true }) ?? '';
if (omit !== 'hex') hex.value = color.toHex();
if (omit !== 'rgb') rgb.value = color.toRgbString();
if (omit !== 'hsl') hsl.value = color.toHslString();
if (omit !== 'hwb') hwb.value = color.toHwbString();
if (omit !== 'cmyk') cmyk.value = color.toCmykString();
if (omit !== 'lch') lch.value = color.toLchString();
} catch {
//
}
}
onInputUpdated(hex.value, 'hex');

View File

@@ -1,13 +1,15 @@
<template>
<n-card>
<n-form-item
class="cron"
:show-label="false"
:feedback="cronValidation.message"
:validation-status="cronValidation.status"
>
<n-input v-model:value="cron" size="large" placeholder="* * * * *" />
</n-form-item>
<c-card>
<div mx-auto max-w-sm>
<c-input-text
v-model:value="cron"
size="large"
placeholder="* * * * *"
:validation-rules="cronValidationRules"
mb-3
/>
</div>
<div class="cron-string">
{{ cronString }}
</div>
@@ -27,8 +29,8 @@
</n-form-item>
</n-form>
</n-space>
</n-card>
<n-card>
</c-card>
<c-card>
<pre>
[optional] seconds (0 - 59)
| minute (0 - 59)
@@ -41,7 +43,7 @@
>
<n-space v-if="styleStore.isSmallScreen" vertical>
<n-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" embedded :bordered="false">
<c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" important:border-none>
<div>
Symbol: <strong>{{ symbol }}</strong>
</div>
@@ -57,7 +59,7 @@
<div>
Equivalent: <strong>{{ equivalent }}</strong>
</div>
</n-card>
</c-card>
</n-space>
<n-table v-else size="small">
<thead>
@@ -79,14 +81,13 @@
</tr>
</tbody>
</n-table>
</n-card>
</c-card>
</template>
<script setup lang="ts">
import cronstrue from 'cronstrue';
import { isValidCron } from 'cron-validator';
import { computed, reactive, ref } from 'vue';
import { useValidation } from '@/composable/validation';
import { useStyleStore } from '@/stores/style.store';
function isCronValid(v: string) {
@@ -185,30 +186,20 @@ const cronString = computed(() => {
return ' ';
});
const cronValidation = useValidation({
source: cron,
rules: [
{
validator: (value) => isCronValid(value),
message: 'This cron is invalid',
},
],
});
const cronValidationRules = [
{
validator: (value: string) => isCronValid(value),
message: 'This cron is invalid',
},
];
</script>
<style lang="less" scoped>
.cron {
::v-deep(input) {
font-size: 30px;
font-family: monospace;
padding: 5px;
text-align: center;
margin: auto;
max-width: 400px;
display: block;
.n-input {
font-size: 30px;
font-family: monospace;
padding: 5px;
}
}
.cron-string {

View File

@@ -1,13 +1,14 @@
<template>
<div>
<n-form-item :show-label="false" v-bind="validation.attrs">
<n-form-item :show-label="false" v-bind="validation.attrs as any">
<n-input-group>
<n-input
v-model:value="inputDate"
autofocus
:on-input="onDateInputChanged"
placeholder="Put you date string here..."
clearable
:input-props="{ 'data-test-id': 'date-time-converter-input' }"
:input-props="{ 'data-test-id': 'date-time-converter-input' } as any"
/>
<n-select
@@ -19,16 +20,19 @@
</n-input-group>
</n-form-item>
<n-divider style="margin-top: 0" />
<div v-for="{ name, fromDate } in formats" :key="name" mt-1>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> {{ name }}: </n-input-group-label>
<input-copyable
:value="formatDateUsingFormatter(fromDate, normalizedDate)"
placeholder="Invalid date..."
:input-props="{ 'data-test-id': name }"
/>
</n-input-group>
</div>
<input-copyable
v-for="{ name, fromDate } in formats"
:key="name"
:label="name"
label-width="150px"
label-position="left"
label-align="right"
:value="formatDateUsingFormatter(fromDate, normalizedDate)"
placeholder="Invalid date..."
:test-id="name"
readonly
mt-2
/>
</div>
</template>

View File

@@ -1,22 +1,20 @@
<template>
<n-card v-for="{ name, information } in sections" :key="name" :title="name">
<c-card v-for="{ name, information } in sections" :key="name" :title="name">
<n-grid cols="1 400:2" x-gap="12" y-gap="12">
<n-gi v-for="{ label, value: { value } } in information" :key="label" class="information">
<n-card :bordered="false" embedded>
<div class="label">
{{ label }}
</div>
<div class="label">
{{ label }}
</div>
<div class="value">
<n-ellipsis v-if="value">
{{ value }}
</n-ellipsis>
<div v-else class="undefined-value">unknown</div>
</div>
</n-card>
<div class="value">
<n-ellipsis v-if="value">
{{ value }}
</n-ellipsis>
<div v-else class="undefined-value">unknown</div>
</div>
</n-gi>
</n-grid>
</n-card>
</c-card>
</template>
<script setup lang="ts">
@@ -81,6 +79,10 @@ const sections = [
<style lang="less" scoped>
.information {
padding: 14px 16px;
border-radius: 4px;
background-color: #aaaaaa11;
.label {
font-size: 14px;
opacity: 0.8;

View File

@@ -13,15 +13,13 @@
<n-divider />
<textarea-copyable :value="dockerCompose" language="yaml" />
<br />
<br />
<n-space justify="center">
<n-button :disabled="dockerCompose === ''" secondary @click="download"> Download docker-compose.yml </n-button>
<n-space justify="center" mt-5>
<c-button :disabled="dockerCompose === ''" secondary @click="download"> Download docker-compose.yml </c-button>
</n-space>
<div v-if="notComposable.length > 0">
<br />
<n-alert title="This options are not translatable to docker-compose" type="info">
<n-alert title="This options are not translatable to docker-compose" type="info" mt-5>
<ul>
<li v-for="(message, index) of notComposable" :key="index">{{ message }}</li>
</ul>
@@ -29,10 +27,10 @@
</div>
<div v-if="notImplemented.length > 0">
<br />
<n-alert
title="This options are not yet implemented and therefore haven't been translated to docker-compose"
type="warning"
mt-5
>
<ul>
<li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li>
@@ -41,8 +39,7 @@
</div>
<div v-if="errors.length > 0">
<br />
<n-alert title="The following errors occured" type="error">
<n-alert title="The following errors occured" type="error" mt-5>
<ul>
<li v-for="(message, index) of errors" :key="index">{{ message }}</li>
</ul>

View File

@@ -1,5 +1,5 @@
<template>
<n-card title="Encrypt">
<c-card title="Encrypt">
<n-space item-style="flex: 1 1 0">
<n-form-item label="Your text:" :show-feedback="false">
<n-input
@@ -7,12 +7,15 @@
type="textarea"
placeholder="The string to cypher"
:autosize="{ minRows: 4 }"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</n-form-item>
<n-space vertical>
<n-form-item label="Your secret key:" :show-feedback="false">
<n-input v-model:value="cypherSecret" />
</n-form-item>
<c-input-text v-model:value="cypherSecret" label="Your secret key:" clearable raw-text />
<n-form-item label="Encryption algorithm:" :show-feedback="false">
<n-select
v-model:value="cypherAlgo"
@@ -21,8 +24,7 @@
</n-form-item>
</n-space>
</n-space>
<br />
<n-form-item label="Your text encrypted:" :show-feedback="false">
<n-form-item label="Your text encrypted:" :show-feedback="false" mt-5>
<n-input
:value="cypherOutput"
type="textarea"
@@ -35,8 +37,8 @@
spellcheck="false"
/>
</n-form-item>
</n-card>
<n-card title="Decrypt">
</c-card>
<c-card title="Decrypt">
<n-space item-style="flex: 1 1 0">
<n-form-item label="Your encrypted text:" :show-feedback="false">
<n-input
@@ -44,12 +46,15 @@
type="textarea"
placeholder="The string to cypher"
:autosize="{ minRows: 4 }"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</n-form-item>
<n-space vertical>
<n-form-item label="Your secret key:" :show-feedback="false">
<n-input v-model:value="decryptSecret" />
</n-form-item>
<c-input-text v-model:value="decryptSecret" label="Your secret key:" clearable raw-text />
<n-form-item label="Encryption algorithm:" :show-feedback="false">
<n-select
v-model:value="decryptAlgo"
@@ -58,8 +63,7 @@
</n-form-item>
</n-space>
</n-space>
<br />
<n-form-item label="Your decrypted text:" :show-feedback="false">
<n-form-item label="Your decrypted text:" :show-feedback="false" mt-5>
<n-input
:value="decryptOutput"
type="textarea"
@@ -72,7 +76,7 @@
spellcheck="false"
/>
</n-form-item>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -5,7 +5,6 @@
hours and 10 minutes to wash them all, and if you start now, you'll end
{{ endAt }}.
</n-text>
<br />
<n-divider />
<n-space item-style="flex:1 1 0">
<div>
@@ -38,12 +37,12 @@
<n-divider />
<n-space vertical>
<n-card>
<c-card>
<n-statistic label="Total duration">{{ formatMsDuration(durationMs) }}</n-statistic>
</n-card>
<n-card>
</c-card>
<c-card>
<n-statistic label="It will end ">{{ endAt }}</n-statistic>
</n-card>
</c-card>
</n-space>
</div>
</n-space>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<n-card>
<c-card>
<n-input v-model:value="clearText" type="textarea" placeholder="Your string to hash..." rows="3" />
<n-divider />
@@ -35,7 +35,7 @@
<input-copyable :value="hashText(algo, clearText)" readonly />
</n-input-group>
</div>
</n-card>
</c-card>
</div>
</template>

View File

@@ -43,7 +43,7 @@
<n-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." />
</n-form-item>
<n-space justify="center">
<n-button secondary @click="copy()">Copy HMAC</n-button>
<c-button @click="copy()">Copy HMAC</c-button>
</n-space>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<n-card title="Escape html entities">
<c-card title="Escape html entities">
<n-form-item label="Your string :">
<n-input
v-model:value="escapeInput"
@@ -20,10 +20,10 @@
</n-form-item>
<n-space justify="center">
<n-button secondary @click="copyEscaped"> Copy </n-button>
<c-button @click="copyEscaped"> Copy </c-button>
</n-space>
</n-card>
<n-card title="Unescape html entities">
</c-card>
<c-card title="Unescape html entities">
<n-form-item label="Your escaped string :">
<n-input
v-model:value="unescapeInput"
@@ -44,9 +44,9 @@
</n-form-item>
<n-space justify="center">
<n-button secondary @click="copyUnescaped"> Copy </n-button>
<c-button @click="copyUnescaped"> Copy </c-button>
</n-space>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -1,12 +1,12 @@
<template>
<n-card v-if="editor" class="editor">
<template #header>
<menu-bar class="editor-header" :editor="editor" />
<n-divider style="margin-top: 0" />
</template>
<c-card v-if="editor" important:p0>
<menu-bar class="editor-header" :editor="editor" />
<n-divider style="margin-top: 0" />
<editor-content class="editor-content" :editor="editor" />
</n-card>
<div px8 pb6>
<editor-content class="editor-content" :editor="editor" />
</div>
</c-card>
</template>
<script setup lang="ts">
@@ -34,10 +34,6 @@ tryOnBeforeUnmount(() => {
</script>
<style scoped lang="less">
::v-deep(.n-card-header) {
padding: 0;
}
::v-deep(.ProseMirror-focused) {
outline: none;
}

View File

@@ -1,11 +1,9 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<n-button circle quaternary :type="isActive?.() ? 'primary' : 'default'" @click="action">
<template #icon>
<n-icon :component="icon" />
</template>
</n-button>
<c-button circle variant="text" :type="isActive?.() ? 'primary' : 'default'" @click="action">
<n-icon :component="icon" />
</c-button>
</template>
{{ title }}

View File

@@ -0,0 +1,432 @@
export const codesByCategories: {
category: string;
codes: {
code: number;
name: string;
description: string;
type: 'HTTP' | 'WebDav';
}[];
}[] = [
{
category: '1xx informational response',
codes: [
{
code: 100,
name: 'Continue',
description: 'Waiting for the client to emit the body of the request.',
type: 'HTTP',
},
{
code: 101,
name: 'Switching Protocols',
description: 'The server has agreed to change protocol.',
type: 'HTTP',
},
{
code: 102,
name: 'Processing',
description: 'The server is processing the request, but no response is available yet.',
type: 'WebDav',
},
{
code: 103,
name: 'Early Hints',
description: 'The server returns some response headers before final HTTP message.',
type: 'HTTP',
},
],
},
{
category: '2xx success',
codes: [
{
code: 200,
name: 'OK',
description: 'Standard response for successful HTTP requests.',
type: 'HTTP',
},
{
code: 201,
name: 'Created',
description: 'The request has been fulfilled, resulting in the creation of a new resource.',
type: 'HTTP',
},
{
code: 202,
name: 'Accepted',
description: 'The request has been accepted for processing, but the processing has not been completed.',
type: 'HTTP',
},
{
code: 203,
name: 'Non-Authoritative Information',
description:
'The request is successful but the content of the original request has been modified by a transforming proxy.',
type: 'HTTP',
},
{
code: 204,
name: 'No Content',
description: 'The server successfully processed the request and is not returning any content.',
type: 'HTTP',
},
{
code: 205,
name: 'Reset Content',
description: 'The server indicates to reinitialize the document view which sent this request.',
type: 'HTTP',
},
{
code: 206,
name: 'Partial Content',
description: 'The server is delivering only part of the resource due to a range header sent by the client.',
type: 'HTTP',
},
{
code: 207,
name: 'Multi-Status',
description:
'The message body that follows is an XML message and can contain a number of separate response codes.',
type: 'WebDav',
},
{
code: 208,
name: 'Already Reported',
description:
'The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response.',
type: 'WebDav',
},
{
code: 226,
name: 'IM Used',
description:
'The server has fulfilled a request for the resource, and the response is a representation of the result.',
type: 'HTTP',
},
],
},
{
category: '3xx redirection',
codes: [
{
code: 300,
name: 'Multiple Choices',
description: 'Indicates multiple options for the resource that the client may follow.',
type: 'HTTP',
},
{
code: 301,
name: 'Moved Permanently',
description: 'This and all future requests should be directed to the given URI.',
type: 'HTTP',
},
{
code: 302,
name: 'Found',
description: 'Redirect to another URL. This is an example of industry practice contradicting the standard.',
type: 'HTTP',
},
{
code: 303,
name: 'See Other',
description: 'The response to the request can be found under another URI using a GET method.',
type: 'HTTP',
},
{
code: 304,
name: 'Not Modified',
description:
'Indicates that the resource has not been modified since the version specified by the request headers.',
type: 'HTTP',
},
{
code: 305,
name: 'Use Proxy',
description:
'The requested resource is available only through a proxy, the address for which is provided in the response.',
type: 'HTTP',
},
{
code: 306,
name: 'Switch Proxy',
description: 'No longer used. Originally meant "Subsequent requests should use the specified proxy."',
type: 'HTTP',
},
{
code: 307,
name: 'Temporary Redirect',
description:
'In this case, the request should be repeated with another URI; however, future requests should still use the original URI.',
type: 'HTTP',
},
{
code: 308,
name: 'Permanent Redirect',
description: 'The request and all future requests should be repeated using another URI.',
type: 'HTTP',
},
],
},
{
category: '4xx client error',
codes: [
{
code: 400,
name: 'Bad Request',
description: 'The server cannot or will not process the request due to an apparent client error.',
type: 'HTTP',
},
{
code: 401,
name: 'Unauthorized',
description:
'Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided.',
type: 'HTTP',
},
{
code: 402,
name: 'Payment Required',
description:
'Reserved for future use. The original intention was that this code might be used as part of some form of digital cash or micropayment scheme.',
type: 'HTTP',
},
{
code: 403,
name: 'Forbidden',
description:
'The request was valid, but the server is refusing action. The user might not have the necessary permissions for a resource.',
type: 'HTTP',
},
{
code: 404,
name: 'Not Found',
description: 'The requested resource could not be found but may be available in the future.',
type: 'HTTP',
},
{
code: 405,
name: 'Method Not Allowed',
description: 'A request method is not supported for the requested resource.',
type: 'HTTP',
},
{
code: 406,
name: 'Not Acceptable',
description:
'The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request.',
type: 'HTTP',
},
{
code: 407,
name: 'Proxy Authentication Required',
description: 'The client must first authenticate itself with the proxy.',
type: 'HTTP',
},
{
code: 408,
name: 'Request Timeout',
description: 'The server timed out waiting for the request.',
type: 'HTTP',
},
{
code: 409,
name: 'Conflict',
description:
'Indicates that the request could not be processed because of conflict in the request, such as an edit conflict.',
type: 'HTTP',
},
{
code: 410,
name: 'Gone',
description: 'Indicates that the resource requested is no longer available and will not be available again.',
type: 'HTTP',
},
{
code: 411,
name: 'Length Required',
description:
'The request did not specify the length of its content, which is required by the requested resource.',
type: 'HTTP',
},
{
code: 412,
name: 'Precondition Failed',
description: 'The server does not meet one of the preconditions that the requester put on the request.',
type: 'HTTP',
},
{
code: 413,
name: 'Payload Too Large',
description: 'The request is larger than the server is willing or able to process.',
type: 'HTTP',
},
{
code: 414,
name: 'URI Too Long',
description: 'The URI provided was too long for the server to process.',
type: 'HTTP',
},
{
code: 415,
name: 'Unsupported Media Type',
description: 'The request entity has a media type which the server or resource does not support.',
type: 'HTTP',
},
{
code: 416,
name: 'Range Not Satisfiable',
description: 'The client has asked for a portion of the file, but the server cannot supply that portion.',
type: 'HTTP',
},
{
code: 417,
name: 'Expectation Failed',
description: 'The server cannot meet the requirements of the Expect request-header field.',
type: 'HTTP',
},
{
code: 418,
name: "I'm a teapot",
description: 'The server refuses the attempt to brew coffee with a teapot.',
type: 'HTTP',
},
{
code: 421,
name: 'Misdirected Request',
description: 'The request was directed at a server that is not able to produce a response.',
type: 'HTTP',
},
{
code: 422,
name: 'Unprocessable Entity',
description: 'The request was well-formed but was unable to be followed due to semantic errors.',
type: 'HTTP',
},
{
code: 423,
name: 'Locked',
description: 'The resource that is being accessed is locked.',
type: 'HTTP',
},
{
code: 424,
name: 'Failed Dependency',
description: 'The request failed due to failure of a previous request.',
type: 'HTTP',
},
{
code: 425,
name: 'Too Early',
description: 'Indicates that the server is unwilling to risk processing a request that might be replayed.',
type: 'HTTP',
},
{
code: 426,
name: 'Upgrade Required',
description: 'The client should switch to a different protocol such as TLS/1.0.',
type: 'HTTP',
},
{
code: 428,
name: 'Precondition Required',
description: 'The origin server requires the request to be conditional.',
type: 'HTTP',
},
{
code: 429,
name: 'Too Many Requests',
description: 'The user has sent too many requests in a given amount of time.',
type: 'HTTP',
},
{
code: 431,
name: 'Request Header Fields Too Large',
description:
'The server is unwilling to process the request because either an individual header field, or all the header fields collectively, are too large.',
type: 'HTTP',
},
{
code: 451,
name: 'Unavailable For Legal Reasons',
description:
'A server operator has received a legal demand to deny access to a resource or to a set of resources that includes the requested resource.',
type: 'HTTP',
},
],
},
{
category: '5xx server error',
codes: [
{
code: 500,
name: 'Internal Server Error',
description:
'A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.',
type: 'HTTP',
},
{
code: 501,
name: 'Not Implemented',
description:
'The server either does not recognize the request method, or it lacks the ability to fulfill the request.',
type: 'HTTP',
},
{
code: 502,
name: 'Bad Gateway',
description:
'The server was acting as a gateway or proxy and received an invalid response from the upstream server.',
type: 'HTTP',
},
{
code: 503,
name: 'Service Unavailable',
description: 'The server is currently unavailable (because it is overloaded or down for maintenance).',
type: 'HTTP',
},
{
code: 504,
name: 'Gateway Timeout',
description:
'The server was acting as a gateway or proxy and did not receive a timely response from the upstream server.',
type: 'HTTP',
},
{
code: 505,
name: 'HTTP Version Not Supported',
description: 'The server does not support the HTTP protocol version used in the request.',
type: 'HTTP',
},
{
code: 506,
name: 'Variant Also Negotiates',
description: 'Transparent content negotiation for the request results in a circular reference.',
type: 'HTTP',
},
{
code: 507,
name: 'Insufficient Storage',
description: 'The server is unable to store the representation needed to complete the request.',
type: 'HTTP',
},
{
code: 508,
name: 'Loop Detected',
description: 'The server detected an infinite loop while processing the request.',
type: 'HTTP',
},
{
code: 510,
name: 'Not Extended',
description: 'Further extensions to the request are required for the server to fulfill it.',
type: 'HTTP',
},
{
code: 511,
name: 'Network Authentication Required',
description: 'The client needs to authenticate to gain network access.',
type: 'HTTP',
},
],
},
];

View File

@@ -0,0 +1,11 @@
import { test, expect } from '@playwright/test';
test.describe('Tool - Http status codes', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/http-status-codes');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('HTTP status codes - IT Tools');
});
});

View File

@@ -0,0 +1,57 @@
<template>
<div>
<n-form-item :show-label="false">
<n-input
v-model:value="search"
placeholder="Search http status..."
size="large"
autofocus
mb-10
autocomplete="off"
autocorrect="off"
autocapitalize="off"
>
<template #prefix>
<n-icon :component="SearchRound" />
</template>
</n-input>
</n-form-item>
<div v-for="{ codes, category } of codesByCategoryFiltered" :key="category" mb-8>
<n-h2> {{ category }} </n-h2>
<c-card v-for="{ code, description, name, type } of codes" :key="code" mb-2>
<n-space align="center">
<n-text strong text-lg> {{ code }} {{ name }} </n-text>
</n-space>
<n-text depth="3">{{ description }} {{ type !== 'HTTP' ? `For ${type}.` : '' }}</n-text>
</c-card>
</div>
</div>
</template>
<script setup lang="ts">
import { useFuzzySearch } from '@/composable/fuzzySearch';
import { SearchRound } from '@vicons/material';
import { codesByCategories } from './http-status-codes.constants';
const search = ref('');
const { searchResult } = useFuzzySearch({
search,
data: codesByCategories.flatMap(({ codes, category }) => codes.map((code) => ({ ...code, category }))),
options: {
keys: [{ name: 'code', weight: 3 }, { name: 'name', weight: 2 }, 'description', 'category'],
},
});
const codesByCategoryFiltered = computed(() => {
if (!search.value) {
return codesByCategories;
}
return [{ category: 'Search results', codes: searchResult.value }];
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,19 @@
import { HttpRound } from '@vicons/material';
import { defineTool } from '../tool';
import { codesByCategories } from './http-status-codes.constants';
export const tool = defineTool({
name: 'HTTP status codes',
path: '/http-status-codes',
description: 'The list of all HTTP status codes their name and their meaning.',
keywords: [
'http',
'status',
'codes',
...codesByCategories.flatMap(({ codes }) => codes.flatMap(({ code, name }) => [String(code), name])),
],
component: () => import('./http-status-codes.vue'),
icon: HttpRound,
createdAt: new Date('2023-04-13'),
});

View File

@@ -1,6 +1,11 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as listConverter } from './list-converter';
import { tool as phoneParserAndFormatter } from './phone-parser-and-formatter';
import { tool as jsonDiff } from './json-diff';
import { tool as ipv4RangeExpander } from './ipv4-range-expander';
import { tool as httpStatusCodes } from './http-status-codes';
import { tool as yamlToJson } from './yaml-to-json-converter';
import { tool as jsonToYaml } from './json-to-yaml-converter';
import { tool as ipv6UlaGenerator } from './ipv6-ula-generator';
@@ -70,6 +75,7 @@ export const toolsByCategory: ToolCategory[] = [
textToNatoAlphabet,
yamlToJson,
jsonToYaml,
listConverter,
],
},
{
@@ -88,6 +94,8 @@ export const toolsByCategory: ToolCategory[] = [
slugifyString,
htmlWysiwygEditor,
userAgentParser,
httpStatusCodes,
jsonDiff,
],
},
{
@@ -109,7 +117,7 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Network',
components: [ipv4SubnetCalculator, ipv4AddressConverter, macAddressLookup, ipv6UlaGenerator],
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, ipv6UlaGenerator],
},
{
name: 'Math',
@@ -123,6 +131,10 @@ export const toolsByCategory: ToolCategory[] = [
name: 'Text',
components: [loremIpsumGenerator, textStatistics],
},
{
name: 'Data',
components: [phoneParserAndFormatter],
},
];
export const tools = toolsByCategory.flatMap(({ components }) => components);

View File

@@ -1,6 +1,6 @@
<template>
<div>
<n-card>
<c-card>
<div v-if="styleStore.isSmallScreen">
<n-input-group>
<n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
@@ -22,60 +22,55 @@
<n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert>
<n-divider />
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Binary (2): </n-input-group-label>
<input-copyable
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
readonly
placeholder="Binary version will be here..."
/>
</n-input-group>
<input-copyable
label="Binary (2)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
placeholder="Binary version will be here..."
/>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Octal (8): </n-input-group-label>
<input-copyable
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
readonly
placeholder="Octal version will be here..."
/>
</n-input-group>
<input-copyable
label="Octal (8)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
placeholder="Octal version will be here..."
/>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Decimal (10): </n-input-group-label>
<input-copyable
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
readonly
placeholder="Decimal version will be here..."
/>
</n-input-group>
<input-copyable
label="Decimal (10)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
placeholder="Decimal version will be here..."
/>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Hexadecimal (16): </n-input-group-label>
<input-copyable
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })"
readonly
placeholder="Decimal version will be here..."
/>
</n-input-group>
<input-copyable
label="Hexadecimal (16)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })"
placeholder="Hexadecimal version will be here..."
/>
<input-copyable
label="Base64 (64)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })"
placeholder="Base64 version will be here..."
/>
<div flex items-baseline>
<n-input-group style="width: 160px; margin-right: 10px">
<n-input-group-label> Custom: </n-input-group-label>
<n-input-number v-model:value="outputBase" max="64" min="2" />
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Base64 (64): </n-input-group-label>
<input-copyable
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })"
readonly
placeholder="Base64 version will be here..."
/>
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 85px"> Custom: </n-input-group-label>
<n-input-number v-model:value="outputBase" style="flex: 0 0 86px" max="64" min="2" />
<input-copyable
flex-1
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })"
readonly
:placeholder="`Base ${outputBase} will be here...`"
/>
</n-input-group>
</n-card>
</div>
</c-card>
</div>
</template>
@@ -88,6 +83,14 @@ import InputCopyable from '../../components/InputCopyable.vue';
const styleStore = useStyleStore();
const inputProps = {
labelPosition: 'left',
labelWidth: '170px',
labelAlign: 'right',
readonly: true,
'mb-2': '',
} as const;
const input = ref('42');
const inputBase = ref(10);
const outputBase = ref(42);

View File

@@ -1,23 +1,20 @@
<template>
<div>
<n-form-item label="An ipv4 address:" v-bind="validationAttrs">
<n-input v-model:value="rawIpAddress" placeholder="An ipv4 address..." />
</n-form-item>
<c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." readonly />
<n-divider style="margin-top: 0" mt-0 />
<n-divider />
<n-form-item
<input-copyable
v-for="{ label, value } of convertedSections"
:key="label"
:label="label"
label-placement="left"
label-width="100"
>
<input-copyable
:value="validationAttrs.validationStatus === 'error' ? '' : value"
placeholder="Set a correct ipv4 address"
/>
</n-form-item>
label-position="left"
label-width="100px"
label-align="right"
mb-2
:value="validationAttrs.validationStatus === 'error' ? '' : value"
placeholder="Set a correct ipv4 address"
/>
</div>
</template>
@@ -33,7 +30,7 @@ const convertedSections = computed(() => {
return [
{
label: 'Decimal : ',
label: 'Decimal: ',
value: String(ipInDecimal),
},
{

View File

@@ -0,0 +1,13 @@
import { UnfoldMoreOutlined } from '@vicons/material';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'IPv4 range expander',
path: '/ipv4-range-expander',
description:
'Given a start and an end IPv4 address this tool calculates a valid IPv4 network with its CIDR notation.',
keywords: ['ipv4', 'range', 'expander', 'subnet', 'creator', 'cidr'],
component: () => import('./ipv4-range-expander.vue'),
icon: UnfoldMoreOutlined,
createdAt: new Date('2023-04-19'),
});

View File

@@ -0,0 +1,32 @@
import { test, expect } from '@playwright/test';
test.describe('Tool - IPv4 range expander', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/ipv4-range-expander');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('IPv4 range expander - IT Tools');
});
test('Calculates correct for valid input', async ({ page }) => {
await page.getByPlaceholder('Start IPv4 address...').fill('192.168.1.1');
await page.getByPlaceholder('End IPv4 address...').fill('192.168.7.255');
expect(await page.getByTestId('start-address.old').textContent()).toEqual('192.168.1.1');
expect(await page.getByTestId('start-address.new').textContent()).toEqual('192.168.0.0');
expect(await page.getByTestId('end-address.old').textContent()).toEqual('192.168.7.255');
expect(await page.getByTestId('end-address.new').textContent()).toEqual('192.168.7.255');
expect(await page.getByTestId('addresses-in-range.old').textContent()).toEqual('1,791');
expect(await page.getByTestId('addresses-in-range.new').textContent()).toEqual('2,048');
expect(await page.getByTestId('cidr.old').textContent()).toEqual('');
expect(await page.getByTestId('cidr.new').textContent()).toEqual('192.168.0.0/21');
});
test('Hides result for invalid input', async ({ page }) => {
await page.getByPlaceholder('Start IPv4 address...').fill('192.168.1.1');
await page.getByPlaceholder('End IPv4 address...').fill('192.168.0.255');
await expect(page.getByTestId('result')).not.toBeVisible();
});
});

View File

@@ -0,0 +1,21 @@
import { expect, describe, it } from 'vitest';
import { calculateCidr } from './ipv4-range-expander.service';
describe('ipv4RangeExpander', () => {
describe('when there are two valid ipv4 addresses given', () => {
it('should calculate valid cidr for given addresses', () => {
const result = calculateCidr({ startIp: '192.168.1.1', endIp: '192.168.7.255' });
expect(result).toBeDefined();
expect(result?.oldSize).toEqual(1791);
expect(result?.newSize).toEqual(2048);
expect(result?.newStart).toEqual('192.168.0.0');
expect(result?.newEnd).toEqual('192.168.7.255');
expect(result?.newCidr).toEqual('192.168.0.0/21');
});
it('should return empty result for invalid input', () => {
expect(calculateCidr({ startIp: '192.168.7.1', endIp: '192.168.6.255' })).not.toBeDefined();
});
});
});

View File

@@ -0,0 +1,63 @@
import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types';
import { convertBase } from '../integer-base-converter/integer-base-converter.model';
import { ipv4ToInt } from '../ipv4-address-converter/ipv4-address-converter.service';
export { calculateCidr };
function bits2ip(ipInt: number) {
return (ipInt >>> 24) + '.' + ((ipInt >> 16) & 255) + '.' + ((ipInt >> 8) & 255) + '.' + (ipInt & 255);
}
function getRangesize(start: string, end: string) {
if (start == null || end == null) return -1;
return 1 + parseInt(end, 2) - parseInt(start, 2);
}
function getCidr(start: string, end: string) {
if (start == null || end == null) return null;
const range = getRangesize(start, end);
if (range < 1) {
return null;
}
let mask = 32;
for (let i = 0; i < 32; i++) {
if (start[i] !== end[i]) {
mask = i;
break;
}
}
const newStart = start.substring(0, mask) + '0'.repeat(32 - mask);
const newEnd = end.substring(0, mask) + '1'.repeat(32 - mask);
return { start: newStart, end: newEnd, mask: mask };
}
function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) {
const start = convertBase({
value: ipv4ToInt({ ip: startIp }).toString(),
fromBase: 10,
toBase: 2,
});
const end = convertBase({
value: ipv4ToInt({ ip: endIp }).toString(),
fromBase: 10,
toBase: 2,
});
const cidr = getCidr(start, end);
if (cidr != null) {
const result: Ipv4RangeExpanderResult = {};
result.newEnd = bits2ip(parseInt(cidr.end, 2));
result.newStart = bits2ip(parseInt(cidr.start, 2));
result.newCidr = result.newStart + '/' + cidr.mask;
result.newSize = getRangesize(cidr.start, cidr.end);
result.oldSize = getRangesize(start, end);
return result;
}
return undefined;
}

View File

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

View File

@@ -0,0 +1,110 @@
<template>
<div>
<n-space item-style="flex:1 1 0">
<div>
<n-space item-style="flex:1 1 0">
<n-form-item label="Start address" v-bind="startIpValidation.attrs as any">
<n-input v-model:value="rawStartAddress" placeholder="Start IPv4 address..." />
</n-form-item>
<n-form-item label="End address" v-bind="endIpValidation.attrs as any">
<n-input v-model:value="rawEndAddress" placeholder="End IPv4 address..." />
</n-form-item>
</n-space>
<n-table v-if="showResult" data-test-id="result">
<thead>
<tr>
<th scope="col">&nbsp;</th>
<th scope="col">old value</th>
<th scope="col">new value</th>
</tr>
</thead>
<tbody>
<result-row
v-for="{ label, getOldValue, getNewValue } in calculatedValues"
:key="label"
:label="label"
:old-value="getOldValue(result)"
:new-value="getNewValue(result)"
/>
</tbody>
</n-table>
<n-alert
v-else-if="startIpValidation.isValid && endIpValidation.isValid"
title="Invalid combination of start and end IPv4 address"
type="error"
>
<n-space vertical>
<n-text depth="3">
The end IPv4 address is lower than the start IPv4 address. This is not valid and no result could be
calculated. In the most cases the solution to solve this problem is to change start and end address.
</n-text>
<c-button @click="onSwitchStartEndClicked">
<n-icon mr-2 :component="Exchange" depth="3" size="22" />
Switch start and end IPv4 address
</c-button>
</n-space>
</n-alert>
</div>
</n-space>
</div>
</template>
<script setup lang="ts">
import { useValidation } from '@/composable/validation';
import { Exchange } from '@vicons/tabler';
import { isValidIpv4 } from '../ipv4-address-converter/ipv4-address-converter.service';
import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types';
import { calculateCidr } from './ipv4-range-expander.service';
import ResultRow from './result-row.vue';
const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1');
const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255');
const result = computed(() => calculateCidr({ startIp: rawStartAddress.value, endIp: rawEndAddress.value }));
const calculatedValues: {
label: string;
getOldValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined;
getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined;
}[] = [
{
label: 'Start address',
getOldValue: () => rawStartAddress.value,
getNewValue: (result) => result?.newStart,
},
{
label: 'End address',
getOldValue: () => rawEndAddress.value,
getNewValue: (result) => result?.newEnd,
},
{
label: 'Addresses in range',
getOldValue: (result) => result?.oldSize?.toLocaleString(),
getNewValue: (result) => result?.newSize?.toLocaleString(),
},
{
label: 'CIDR',
getOldValue: () => '',
getNewValue: (result) => result?.newCidr,
},
];
const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined);
const startIpValidation = useValidation({
source: rawStartAddress,
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }],
});
const endIpValidation = useValidation({
source: rawEndAddress,
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }],
});
function onSwitchStartEndClicked() {
const tmpStart = rawStartAddress.value;
rawStartAddress.value = rawEndAddress.value;
rawEndAddress.value = tmpStart;
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,27 @@
<template>
<tr>
<td>
<n-text strong>{{ label }}</n-text>
</td>
<td :data-test-id="testId + '.old'"><span-copyable :value="oldValue" class="monospace" /></td>
<td :data-test-id="testId + '.new'">
<span-copyable :value="newValue"></span-copyable>
</td>
</tr>
</template>
<script setup lang="ts">
import SpanCopyable from '@/components/SpanCopyable.vue';
import _ from 'lodash';
const props = withDefaults(defineProps<{ label: string; oldValue?: string; newValue?: string }>(), {
label: '',
oldValue: '',
newValue: '',
});
const { label, oldValue, newValue } = toRefs(props);
const testId = computed(() => _.kebabCase(label.value));
</script>
<style scoped lang="less"></style>

View File

@@ -1,8 +1,12 @@
<template>
<div>
<n-form-item label="An IPv4 address with or without mask" v-bind="validationAttrs">
<n-input v-model:value="ip" />
</n-form-item>
<c-input-text
v-model:value="ip"
label="An IPv4 address with or without mask"
placeholder="The ipv4 address..."
:validation-rules="ipValidationRules"
mb-4
/>
<div v-if="networkInfo">
<n-table>
@@ -12,7 +16,7 @@
<n-text strong>{{ label }}</n-text>
</td>
<td>
<copyable-ip-like v-if="getValue(networkInfo)" :ip="getValue(networkInfo)"></copyable-ip-like>
<span-copyable v-if="getValue(networkInfo)" :value="getValue(networkInfo)"></span-copyable>
<n-text v-else depth="3">{{ undefinedFallback }}</n-text>
</td>
</tr>
@@ -20,14 +24,14 @@
</n-table>
<n-space style="margin-top: 14px" justify="space-between">
<n-button tertiary @click="switchToBlock({ count: -1 })">
<c-button @click="switchToBlock({ count: -1 })">
<n-icon :component="ArrowLeft" />
Previous block
</n-button>
<n-button tertiary @click="switchToBlock({ count: 1 })">
</c-button>
<c-button @click="switchToBlock({ count: 1 })">
Next block
<n-icon :component="ArrowRight" />
</n-button>
</c-button>
</n-space>
</div>
</div>
@@ -37,12 +41,11 @@
import { computed } from 'vue';
import { Netmask } from 'netmask';
import { withDefaultOnError } from '@/utils/defaults';
import { useValidation } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean';
import { useStorage } from '@vueuse/core';
import { ArrowLeft, ArrowRight } from '@vicons/tabler';
import SpanCopyable from '@/components/SpanCopyable.vue';
import { getIPClass } from './ipv4-subnet-calculator.models';
import CopyableIpLike from './copyable-ip-like.vue';
const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24');
@@ -50,15 +53,12 @@ const getNetworkInfo = (address: string) => new Netmask(address.trim());
const networkInfo = computed(() => withDefaultOnError(() => getNetworkInfo(ip.value), undefined));
const { attrs: validationAttrs } = useValidation({
source: ip,
rules: [
{
message: 'We cannot parse this address, check the format',
validator: (value) => isNotThrowing(() => getNetworkInfo(value.trim())),
},
],
});
const ipValidationRules = [
{
message: 'We cannot parse this address, check the format',
validator: (value: string) => isNotThrowing(() => getNetworkInfo(value.trim())),
},
];
const sections: {
label: string;

View File

@@ -1,30 +1,32 @@
<template>
<div>
<n-space vertical :size="50">
<n-alert title="Info" type="info">
This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed,
and the lower 40 bits to generate your random ULA.
</n-alert>
<n-alert title="Info" type="info">
This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed,
and the lower 40 bits to generate your random ULA.
</n-alert>
<n-form-item label="MAC address:" v-bind="validationAttrs">
<n-input
v-model:value="macAddress"
size="large"
placeholder="Type a MAC address"
clearable
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</n-form-item>
</n-space>
<c-input-text
v-model:value="macAddress"
placeholder="Type a MAC address"
clearable
label="MAC address:"
raw-text
my-8
:validation="addressValidation"
/>
<div v-if="validationAttrs.validationStatus !== 'error'">
<n-input-group v-for="{ label, value } in calculatedSections" :key="label" style="margin: 5px 0">
<n-input-group-label style="flex: 0 0 160px"> {{ label }} </n-input-group-label>
<input-copyable :value="value" readonly />
</n-input-group>
<div v-if="addressValidation.isValid">
<input-copyable
v-for="{ label, value } in calculatedSections"
:key="label"
:value="value"
:label="label"
label-width="160px"
label-align="right"
label-position="left"
readonly
mb-2
/>
</div>
</div>
</template>
@@ -59,7 +61,7 @@ const calculatedSections = computed(() => {
];
});
const { attrs: validationAttrs } = macAddressValidation(macAddress);
const addressValidation = macAddressValidation(macAddress);
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,119 @@
import _ from 'lodash';
import { useCopy } from '@/composable/copy';
import type { Difference, ArrayDifference, ObjectDifference } from '../json-diff.types';
export const DiffRootViewer = ({ diff }: { diff: Difference }) => {
return (
<div class={'diffs-viewer'}>
<ul>{DiffViewer({ diff, showKeys: false })}</ul>
</div>
);
};
const DiffViewer = ({ diff, showKeys = true }: { diff: Difference; showKeys?: boolean }) => {
const { type, status } = diff;
if (status === 'updated') {
return ComparisonViewer({ diff, showKeys });
}
if (type === 'array') {
return ChildrenViewer({ diff, showKeys, showChildrenKeys: false, openTag: '[', closeTag: ']' });
}
if (type === 'object') {
return ChildrenViewer({ diff, showKeys, openTag: '{', closeTag: '}' });
}
return LineDiffViewer({ diff, showKeys });
};
const LineDiffViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) => {
const { value, key, status, oldValue } = diff;
const valueToDisplay = status === 'removed' ? oldValue : value;
return (
<li>
<span class={[status, 'result']}>
{showKeys && (
<>
<span class={'key'}>{key}</span>
{': '}
</>
)}
{Value({ value: valueToDisplay, status })}
</span>
,
</li>
);
};
const ComparisonViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) => {
const { value, key, oldValue } = diff;
return (
<li class={'updated-line'}>
{showKeys && (
<>
<span class={'key'}>{key}</span>
{': '}
</>
)}
{Value({ value: oldValue, status: 'removed' })}
{Value({ value, status: 'added' })},
</li>
);
};
const ChildrenViewer = ({
diff,
openTag,
closeTag,
showKeys,
showChildrenKeys = true,
}: {
diff: ArrayDifference | ObjectDifference;
showKeys: boolean;
showChildrenKeys?: boolean;
openTag: string;
closeTag: string;
}) => {
const { children, key, status, type } = diff;
return (
<li>
<div class={[type, status]} style={{ display: 'inline-block' }}>
{showKeys && (
<>
<span class={'key'}>{key}</span>
{': '}
</>
)}
{openTag}
{children.length > 0 && <ul>{children.map((diff) => DiffViewer({ diff, showKeys: showChildrenKeys }))}</ul>}
{closeTag + ','}
</div>
</li>
);
};
function formatValue(value: unknown) {
if (_.isNull(value)) {
return 'null';
}
return JSON.stringify(value);
}
const Value = ({ value, status }: { value: unknown; status: string }) => {
const formatedValue = formatValue(value);
const { copy } = useCopy({ source: formatedValue });
return (
<span class={['value', status]} onClick={copy}>
{formatedValue}
</span>
);
};

View File

@@ -0,0 +1,110 @@
<template>
<div v-if="showResults">
<n-space justify="center">
<n-form-item label="Only show differences" label-placement="left">
<n-switch v-model:value="onlyShowDifferences" />
</n-form-item>
</n-space>
<c-card data-test-id="diff-result">
<n-text v-if="jsonAreTheSame" depth="3" block text-center italic> The provided JSONs are the same </n-text>
<diff-root-viewer v-else :diff="result" />
</c-card>
</div>
</template>
<script lang="ts" setup>
import { useAppTheme } from '@/ui/theme/themes';
import _ from 'lodash';
import { DiffRootViewer } from './diff-viewer.models';
import { diff } from '../json-diff.models';
const onlyShowDifferences = ref(false);
const props = defineProps<{ leftJson: unknown; rightJson: unknown }>();
const { leftJson, rightJson } = toRefs(props);
const appTheme = useAppTheme();
const result = computed(() =>
diff(leftJson.value, rightJson.value, { onlyShowDifferences: onlyShowDifferences.value }),
);
const jsonAreTheSame = computed(() => _.isEqual(leftJson.value, rightJson.value));
const showResults = computed(() => !_.isUndefined(leftJson.value) && !_.isUndefined(rightJson.value));
</script>
<style lang="less" scoped>
::v-deep(.diffs-viewer) {
color: v-bind('appTheme.text.mutedColor');
& > ul {
padding-left: 0 !important;
}
ul {
list-style: none;
padding-left: 20px;
margin: 0;
li {
.updated-line {
padding: 3px 0;
}
.result,
.array,
.object,
.value {
&:not(:last-child) {
margin-right: 4px;
}
&.added {
padding: 3px 5px;
border-radius: 4px;
background-color: v-bind('appTheme.success.colorFaded');
color: v-bind('appTheme.success.color');
.key {
color: inherit;
}
}
&.removed {
padding: 3px 5px;
border-radius: 4px;
background-color: v-bind('appTheme.error.colorFaded');
color: v-bind('appTheme.error.color');
.key {
color: inherit;
}
}
}
.value {
cursor: pointer;
border: 1px solid transparent;
transition: border-color 0.2s ease-in-out;
&.added:hover {
border-color: v-bind('appTheme.success.color');
}
&.removed:hover {
border-color: v-bind('appTheme.error.color');
}
}
.added .added,
.removed .removed {
background-color: transparent;
color: inherit;
}
.key {
font-weight: 500;
color: v-bind('appTheme.text.baseColor');
}
}
}
}
</style>

View File

@@ -0,0 +1,12 @@
import { CompareArrowsRound } from '@vicons/material';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'JSON diff',
path: '/json-diff',
description: 'Compare two JSON objects and get the differences between them.',
keywords: ['json', 'diff', 'compare', 'difference', 'object', 'data'],
component: () => import('./json-diff.vue'),
icon: CompareArrowsRound,
createdAt: new Date('2023-04-20'),
});

View File

@@ -0,0 +1,39 @@
import { test, expect } from '@playwright/test';
test.describe('Tool - JSON diff', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/json-diff');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('JSON diff - IT Tools');
});
test('Identical JSONs have a custom result message', async ({ page }) => {
await page.getByTestId('leftJson').fill('{"foo":"bar"}');
await page.getByTestId('rightJson').fill('{ "foo": "bar" } ');
const result = await page.getByTestId('diff-result').innerText();
expect(result).toContain('The provided JSONs are the same');
});
test('Different JSONs have differences listed', async ({ page }) => {
await page.getByTestId('leftJson').fill('{"foo":"bar"}');
await page.getByTestId('rightJson').fill('{"foo":"buz","baz":"qux"}');
const result = await page.getByTestId('diff-result').innerText();
expect(result).toContain(`{\nfoo: "bar""buz",\nbaz: "qux",\n},`);
});
test('Different JSONs have only differences listed when "Only show differences" is checked', async ({ page }) => {
await page.getByTestId('leftJson').fill('{"foo":"bar"}');
await page.getByTestId('rightJson').fill('{"foo":"bar","baz":"qux"}');
await page.getByRole('switch').click();
const result = await page.getByTestId('diff-result').innerText();
expect(result).toContain(`{\nbaz: "qux",\n},`);
});
});

View File

@@ -0,0 +1,80 @@
import { expect, describe, it } from 'vitest';
import { diff } from './json-diff.models';
describe('json-diff models', () => {
describe('diff', () => {
it('list object differences', () => {
const obj = { a: 1, b: 2 };
const newObj = { a: 1, b: 2, c: 3 };
const result = diff(obj, newObj);
expect(result).toEqual({
key: '',
type: 'object',
children: [
{
key: 'a',
type: 'value',
value: 1,
oldValue: 1,
status: 'unchanged',
},
{
key: 'b',
type: 'value',
value: 2,
oldValue: 2,
status: 'unchanged',
},
{
key: 'c',
type: 'value',
value: 3,
oldValue: undefined,
status: 'added',
},
],
oldValue: { a: 1, b: 2 },
value: { a: 1, b: 2, c: 3 },
status: 'children-updated',
});
});
it('list array differences', () => {
const obj = [1, 2];
const newObj = [1, 2, 3];
const result = diff(obj, newObj);
expect(result).toEqual({
key: '',
type: 'array',
children: [
{
key: 0,
type: 'value',
value: 1,
oldValue: 1,
status: 'unchanged',
},
{
key: 1,
type: 'value',
value: 2,
oldValue: 2,
status: 'unchanged',
},
{
key: 2,
type: 'value',
value: 3,
oldValue: undefined,
status: 'added',
},
],
oldValue: [1, 2],
value: [1, 2, 3],
status: 'children-updated',
});
});
});
});

View File

@@ -0,0 +1,140 @@
import _ from 'lodash';
import type { Difference, DifferenceStatus } from './json-diff.types';
export { diff };
function diff(
obj: unknown,
newObj: unknown,
{ onlyShowDifferences = false }: { onlyShowDifferences?: boolean } = {},
): Difference {
if (_.isArray(obj) && _.isArray(newObj)) {
return {
key: '',
type: 'array',
children: diffArrays(obj, newObj, { onlyShowDifferences }),
oldValue: obj,
value: newObj,
status: getStatus(obj, newObj),
};
}
if (_.isObject(obj) && _.isObject(newObj)) {
return {
key: '',
type: 'object',
children: diffObjects(obj as Record<string, unknown>, newObj as Record<string, unknown>, { onlyShowDifferences }),
oldValue: obj,
value: newObj,
status: getStatus(obj, newObj),
};
}
return {
key: '',
type: 'value',
oldValue: obj,
value: newObj,
status: getStatus(obj, newObj),
};
}
function diffObjects(
obj: Record<string, unknown>,
newObj: Record<string, unknown>,
{ onlyShowDifferences = false }: { onlyShowDifferences?: boolean } = {},
): Difference[] {
const keys = Object.keys({ ...obj, ...newObj });
return keys
.map((key) => createDifference(obj?.[key], newObj?.[key], key, { onlyShowDifferences }))
.filter((diff) => !onlyShowDifferences || diff.status !== 'unchanged');
}
function createDifference(
value: unknown,
newValue: unknown,
key: string | number,
{ onlyShowDifferences = false }: { onlyShowDifferences?: boolean } = {},
): Difference {
const type = getType(value);
if (type === 'object') {
return {
key,
type,
children: diffObjects(value as Record<string, unknown>, newValue as Record<string, unknown>, {
onlyShowDifferences,
}),
oldValue: value,
value: newValue,
status: getStatus(value, newValue),
};
}
if (type === 'array') {
return {
key,
type,
children: diffArrays(value as unknown[], newValue as unknown[], { onlyShowDifferences }),
value: newValue,
oldValue: value,
status: getStatus(value, newValue),
};
}
return {
key,
type,
value: newValue,
oldValue: value,
status: getStatus(value, newValue),
};
}
function diffArrays(
arr: unknown[],
newArr: unknown[],
{ onlyShowDifferences = false }: { onlyShowDifferences?: boolean } = {},
): Difference[] {
const maxLength = Math.max(0, arr?.length, newArr?.length);
return Array.from({ length: maxLength }, (_, i) =>
createDifference(arr?.[i], newArr?.[i], i, { onlyShowDifferences }),
).filter((diff) => !onlyShowDifferences || diff.status !== 'unchanged');
}
function getType(value: unknown): 'object' | 'array' | 'value' {
if (value === null) {
return 'value';
}
if (Array.isArray(value)) {
return 'array';
}
if (typeof value === 'object') {
return 'object';
}
return 'value';
}
function getStatus(value: unknown, newValue: unknown): DifferenceStatus {
if (value === undefined) {
return 'added';
}
if (newValue === undefined) {
return 'removed';
}
const bothAreObjects = getType(value) === 'object' && getType(newValue) === 'object';
const bothAreArrays = getType(value) === 'array' && getType(newValue) === 'array';
const bothAreDeepEqual = _.isEqual(value, newValue);
if (bothAreDeepEqual) {
return 'unchanged';
}
if (bothAreObjects || bothAreArrays) {
return 'children-updated';
}
return 'updated';
}

View File

@@ -0,0 +1,29 @@
export type DifferenceStatus = 'added' | 'removed' | 'updated' | 'unchanged' | 'children-updated';
export type ObjectDifference = {
key: string | number;
type: 'object';
children: Difference[];
status: DifferenceStatus;
oldValue: unknown;
value: unknown;
};
export type ValueDifference = {
key: string | number;
type: 'value';
value: unknown;
oldValue: unknown;
status: DifferenceStatus;
};
export type ArrayDifference = {
key: number | string;
type: 'array';
children: Difference[];
status: DifferenceStatus;
oldValue: unknown;
value: unknown;
};
export type Difference = ObjectDifference | ValueDifference | ArrayDifference;

View File

@@ -0,0 +1,59 @@
<template>
<n-form-item label="Your first json" v-bind="leftJsonValidation.attrs as any">
<n-input
v-model:value="rawLeftJson"
placeholder="Paste your first json here..."
type="textarea"
rows="20"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
:input-props="{ 'data-test-id': 'leftJson' } as any"
/>
</n-form-item>
<n-form-item label="Your json to compare" v-bind="rightJsonValidation.attrs as any">
<n-input
v-model:value="rawRightJson"
placeholder="Paste your json to compare here..."
type="textarea"
rows="20"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
:input-props="{ 'data-test-id': 'rightJson' } as any"
/>
</n-form-item>
<DiffsViewer :left-json="leftJson" :right-json="rightJson" />
</template>
<script setup lang="ts">
import JSON5 from 'json5';
import { withDefaultOnError } from '@/utils/defaults';
import { useValidation } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean';
import DiffsViewer from './diff-viewer/diff-viewer.vue';
const rawLeftJson = ref('');
const rawRightJson = ref('');
const leftJson = computed(() => withDefaultOnError(() => JSON5.parse(rawLeftJson.value), undefined));
const rightJson = computed(() => withDefaultOnError(() => JSON5.parse(rawRightJson.value), undefined));
const createJsonValidation = (json: Ref) =>
useValidation({
source: json,
rules: [
{
validator: (value) => value === '' || isNotThrowing(() => JSON5.parse(value)),
message: 'Invalid JSON',
},
],
});
const leftJsonValidation = createJsonValidation(rawLeftJson);
const rightJsonValidation = createJsonValidation(rawRightJson);
</script>

View File

@@ -9,7 +9,7 @@ function sortObjectKeys<T>(obj: T): T {
}
if (Array.isArray(obj)) {
return obj.map(sortObjectKeys) as T;
return obj.map(sortObjectKeys) as unknown as T;
}
return Object.keys(obj)

View File

@@ -1,5 +1,5 @@
<template>
<n-card>
<c-card>
<n-form-item label="JWT to decode" :feedback="validation.message" :validation-status="validation.status">
<n-input v-model:value="rawJwt" type="textarea" placeholder="Put your token here..." rows="5" />
</n-form-item>
@@ -29,7 +29,7 @@
</template>
</tbody>
</n-table>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -1,9 +1,9 @@
<template>
<div>
<n-card style="text-align: center; padding: 40px 0; margin-bottom: 26px">
<c-card style="text-align: center; padding: 40px 0; margin-bottom: 26px">
<n-h2 v-if="event">{{ event.key }}</n-h2>
<n-text strong depth="3">Press the key on your keyboard you want to get info about this key</n-text>
</n-card>
</c-card>
<n-input-group v-for="({ label, value, placeholder }, i) of fields" :key="i" style="margin-bottom: 5px">
<n-input-group-label style="flex: 0 0 150px"> {{ label }} </n-input-group-label>

View File

@@ -0,0 +1,13 @@
import { List } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'List converter',
path: '/list-converter',
description:
'This tool can process column-based data and apply various changes (transpose, add prefix and suffix, reverse list, sort list, lowercase values, truncate values) to each row.',
keywords: ['list', 'converter', 'sort', 'reverse', 'prefix', 'suffix', 'lowercase', 'truncate'],
component: () => import('./list-converter.vue'),
icon: List,
createdAt: new Date('2023-05-07'),
});

View File

@@ -0,0 +1,39 @@
import { test, expect } from '@playwright/test';
test.describe('Tool - List converter', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/list-converter');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('List converter - IT Tools');
});
test('Simple list should be converted with default settings', async ({ page }) => {
await page.getByTestId('input').fill(`1
2
3
4
5`);
const result = await page.getByTestId('area-content').innerText();
expect(result.trim()).toEqual('1, 2, 3, 4, 5');
});
test('Duplicates should be removed, list should be sorted and prefix and suffix list items', async ({ page }) => {
await page.getByTestId('input').fill(`1
2
2
4
4
3
5`);
await page.getByTestId('removeDuplicates').check();
await page.getByTestId('itemPrefix').fill("'");
await page.getByTestId('itemSuffix').fill("'");
const result = await page.getByTestId('area-content').innerText();
expect(result.trim()).toEqual("'1', '2', '4', '3', '5'");
});
});

View File

@@ -0,0 +1,76 @@
import { expect, describe, it } from 'vitest';
import { convert } from './list-converter.models';
import type { ConvertOptions } from './list-converter.types';
describe('list-converter', () => {
describe('convert', () => {
it('should convert a given list', () => {
const options: ConvertOptions = {
separator: ', ',
trimItems: true,
removeDuplicates: true,
itemPrefix: '"',
itemSuffix: '"',
listPrefix: '',
listSuffix: '',
reverseList: false,
sortList: null,
lowerCase: false,
keepLineBreaks: false,
};
const input = `
1
2
3
3
4
`;
expect(convert(input, options)).toEqual('"1", "2", "3", "4"');
});
it('should return an empty value for an empty input', () => {
const options: ConvertOptions = {
separator: ', ',
trimItems: true,
removeDuplicates: true,
itemPrefix: '',
itemSuffix: '',
listPrefix: '',
listSuffix: '',
reverseList: false,
sortList: null,
lowerCase: false,
keepLineBreaks: false,
};
expect(convert('', options)).toEqual('');
});
it('should keep line breaks', () => {
const options: ConvertOptions = {
separator: '',
trimItems: true,
itemPrefix: '<li>',
itemSuffix: '</li>',
listPrefix: '<ul>',
listSuffix: '</ul>',
keepLineBreaks: true,
lowerCase: false,
removeDuplicates: false,
reverseList: false,
sortList: null,
};
const input = `
1
2
3
`;
const expected = `<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>`;
expect(convert(input, options)).toEqual(expected);
});
});
});

View File

@@ -0,0 +1,27 @@
import _ from 'lodash';
import { byOrder } from '@/utils/array';
import type { ConvertOptions } from './list-converter.types';
export { convert };
const whenever =
<T, R>(condition: boolean, fn: (value: T) => R) =>
(value: T) =>
condition ? fn(value) : value;
function convert(list: string, options: ConvertOptions): string {
const lineBreak = options.keepLineBreaks ? '\n' : '';
return _.chain(list)
.thru(whenever(options.lowerCase, (text) => text.toLowerCase()))
.split('\n')
.thru(whenever(options.removeDuplicates, _.uniq))
.thru(whenever(options.reverseList, _.reverse))
.thru(whenever(!_.isNull(options.sortList), (parts) => parts.sort(byOrder({ order: options.sortList }))))
.map(whenever(options.trimItems, _.trim))
.without('')
.map((p) => options.itemPrefix + p + options.itemSuffix)
.join(options.separator + lineBreak)
.thru((text) => [options.listPrefix, text, options.listSuffix].join(lineBreak))
.value();
}

View File

@@ -0,0 +1,15 @@
export type SortOrder = 'asc' | 'desc' | null;
export type ConvertOptions = {
lowerCase: boolean;
trimItems: boolean;
itemPrefix: string;
itemSuffix: string;
listPrefix: string;
listSuffix: string;
reverseList: boolean;
sortList: SortOrder;
removeDuplicates: boolean;
separator: string;
keepLineBreaks: boolean;
};

View File

@@ -0,0 +1,123 @@
<template>
<div style="flex: 0 0 100%">
<n-space item-style="flex: 1 1 0" style="margin: 0 auto; max-width: 600px" justify="center">
<c-card>
<div flex>
<div>
<n-form-item label="Trim list items" label-placement="left" label-width="150" :show-feedback="false" mb-2>
<n-switch v-model:value="conversionConfig.trimItems" />
</n-form-item>
<n-form-item label="Remove duplicates" label-placement="left" label-width="150" :show-feedback="false" mb-2>
<n-switch v-model:value="conversionConfig.removeDuplicates" data-test-id="removeDuplicates" />
</n-form-item>
<n-form-item
label="Convert to lowercase"
label-placement="left"
label-width="150"
:show-feedback="false"
mb-2
>
<n-switch v-model:value="conversionConfig.lowerCase" />
</n-form-item>
<n-form-item label="Keep line breaks" label-placement="left" label-width="150" :show-feedback="false" mb-2>
<n-switch v-model:value="conversionConfig.keepLineBreaks" />
</n-form-item>
</div>
<div flex-1>
<n-form-item label="Sort list" label-placement="left" label-width="120" :show-feedback="false" mb-2>
<n-select
v-model:value="conversionConfig.sortList"
:options="sortOrderOptions"
clearable
w-full
:disabled="conversionConfig.reverseList"
data-test-id="sortList"
placeholder="Sort alphabetically"
/>
</n-form-item>
<c-input-text
v-model:value="conversionConfig.separator"
label="Separator"
label-position="left"
label-width="120px"
label-align="right"
mb-2
placeholder=","
/>
<n-form-item label="Wrap item" label-placement="left" label-width="120" :show-feedback="false" mb-2>
<c-input-text
v-model:value="conversionConfig.itemPrefix"
placeholder="Item prefix"
test-id="itemPrefix"
/>
<c-input-text
v-model:value="conversionConfig.itemSuffix"
placeholder="Item suffix"
test-id="itemSuffix"
/>
</n-form-item>
<n-form-item label="Wrap list" label-placement="left" label-width="120" :show-feedback="false" mb-2>
<c-input-text
v-model:value="conversionConfig.listPrefix"
placeholder="List prefix"
test-id="listPrefix"
/>
<c-input-text
v-model:value="conversionConfig.listSuffix"
placeholder="List suffix"
test-id="listSuffix"
/>
</n-form-item>
</div>
</div>
</c-card>
</n-space>
</div>
<format-transformer
input-label="Your input data"
input-placeholder="Paste your input data here..."
output-label="Your transformed data"
:transformer="transformer"
/>
</template>
<script setup lang="ts">
import { useStorage } from '@vueuse/core';
import { convert } from './list-converter.models';
import type { ConvertOptions } from './list-converter.types';
const sortOrderOptions = [
{
label: 'Sort ascending',
value: 'asc',
disabled: false,
},
{
label: 'Sort descending',
value: 'desc',
disabled: false,
},
];
const conversionConfig = useStorage<ConvertOptions>('list-converter:conversionConfig', {
lowerCase: false,
trimItems: true,
removeDuplicates: true,
keepLineBreaks: false,
itemPrefix: '',
itemSuffix: '',
listPrefix: '',
listSuffix: '',
reverseList: false,
sortList: null,
separator: ', ',
});
const transformer = (value: string) => {
return convert(value, conversionConfig.value);
};
</script>
<style lang="less" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<n-card>
<c-card>
<n-form-item label="Paragraphs" :show-feedback="false" label-width="200" label-placement="left">
<n-slider v-model:value="paragraphs" :step="1" :min="1" :max="20" />
</n-form-item>
@@ -16,15 +16,12 @@
<n-switch v-model:value="asHTML" />
</n-form-item>
<br />
<n-input :value="loremIpsumText" type="textarea" placeholder="Your lorem ipsum..." readonly autosize mt-5 />
<n-input :value="loremIpsumText" type="textarea" placeholder="Your lorem ipsum..." readonly autosize />
<br />
<br />
<n-space justify="center">
<n-button secondary autofocus @click="copy"> Copy </n-button>
<n-space justify="center" mt-5>
<c-button autofocus @click="copy"> Copy </c-button>
</n-space>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -1,6 +1,6 @@
<template>
<div>
<n-form-item label="MAC address:" v-bind="validationAttrs">
<n-form-item label="MAC address:" v-bind="validationAttrs as any">
<n-input
v-model:value="macAddress"
size="large"
@@ -14,17 +14,17 @@
</n-form-item>
<n-form-item label="Vendor info:">
<n-card>
<c-card>
<n-text v-if="details">
<div v-for="(detail, index) of details.split('\n')" :key="index">{{ detail }}</div>
</n-text>
<n-text v-else depth="3" italic>Unknown vendor for this address</n-text>
</n-card>
</c-card>
</n-form-item>
<n-space justify="center">
<n-button :disabled="!details" tertiary> Copy vendor info </n-button>
<c-button :disabled="!details" @click="copy"> Copy vendor info </c-button>
</n-space>
</div>
</template>
@@ -32,6 +32,7 @@
<script setup lang="ts">
import db from 'oui/oui.json';
import { macAddressValidation } from '@/utils/macAddress';
import { useCopy } from '@/composable/copy';
const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '').toUpperCase().substring(0, 6);
@@ -39,6 +40,8 @@ const macAddress = ref('20:37:06:12:34:56');
const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]);
const { attrs: validationAttrs } = macAddressValidation(macAddress);
const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' });
</script>
<style lang="less" scoped></style>

View File

@@ -11,12 +11,10 @@
autocapitalize="off"
spellcheck="false"
/>
<br />
<br />
<n-card v-if="result !== ''" title="Result ">
<c-card v-if="result !== ''" title="Result " mt-5>
{{ result }}
</n-card>
</c-card>
</div>
</template>

View File

@@ -5,7 +5,7 @@
<n-input-group v-for="{ key, type, label, placeholder, ...element } of elements" :key="key">
<n-input-group-label style="flex: 0 0 110px">{{ label }}</n-input-group-label>
<n-input v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" />
<c-input-text v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" clearable />
<n-dynamic-input
v-else-if="type === 'input-multiple'"
v-model:value="metadata[key]"

View File

@@ -1,5 +1,5 @@
<template>
<n-card>
<c-card>
<n-h2 style="margin-bottom: 0">Mime type to extension</n-h2>
<div style="opacity: 0.8">Now witch file extensions are associated to a mime-type</div>
<n-form-item>
@@ -27,9 +27,9 @@
</n-tag>
</div>
</div>
</n-card>
</c-card>
<n-card>
<c-card>
<n-h2 style="margin-bottom: 0">File extension to mime type</n-h2>
<div style="opacity: 0.8">Now witch mime type is associated to a file extension</div>
<n-form-item>
@@ -51,7 +51,7 @@
</n-tag>
</div>
</div>
</n-card>
</c-card>
<div>
<n-table>

View File

@@ -1,19 +1,23 @@
<template>
<div style="max-width: 350px">
<n-form-item label="Secret" v-bind="secretValidationAttrs">
<n-input v-model:value="secret" placeholder="Paste your TOTP secret...">
<template #suffix>
<n-tooltip trigger="hover">
<template #trigger>
<n-button quaternary circle @click="refreshSecret">
<n-icon :component="Refresh" />
</n-button>
</template>
Generate secret token
</n-tooltip>
</template>
</n-input>
</n-form-item>
<c-input-text
v-model:value="secret"
label="Secret"
placeholder="Paste your TOTP secret..."
mb-5
:validation-rules="secretValidationRules"
>
<template #suffix>
<n-tooltip trigger="hover">
<template #trigger>
<c-button circle variant="text" size="small" @click="refreshSecret">
<icon-mdi-refresh />
</c-button>
</template>
Generate secret token
</n-tooltip>
</template>
</c-input-text>
<div>
<token-display :tokens="tokens" style="margin-top: 2px" />
@@ -23,53 +27,56 @@
</div>
<n-space justify="center" vertical align="center" style="margin-top: 10px">
<n-image :src="qrcode"></n-image>
<n-button secondary tag="a" :href="keyUri" target="_blank">Open Key URI in new tab</n-button>
<c-button :href="keyUri" target="_blank">Open Key URI in new tab</c-button>
</n-space>
</div>
<div style="max-width: 350px">
<n-form-item label="Secret in hexadecimal">
<input-copyable :value="base32toHex(secret)" readonly placeholder="Secret in hex will be displayed here" />
</n-form-item>
<input-copyable
label="Secret in hexadecimal"
:value="base32toHex(secret)"
readonly
placeholder="Secret in hex will be displayed here"
mb-5
/>
<n-form-item label="Epoch">
<input-copyable
:value="Math.floor(now / 1000).toString()"
readonly
placeholder="Epoch in sec will be displayed here"
/>
</n-form-item>
<n-form-item label="Iteration" :show-feedback="false">
<n-input-group>
<n-input-group-label style="width: 110px">Count:</n-input-group-label>
<input-copyable
:value="String(getCounterFromTime({ now, timeStep: 30 }))"
readonly
placeholder="Iteration count will be displayed here"
/>
</n-input-group>
</n-form-item>
<input-copyable
label="Epoch"
:value="Math.floor(now / 1000).toString()"
readonly
mb-5
placeholder="Epoch in sec will be displayed here"
/>
<n-form-item label="Iteration" :show-label="false" style="margin-top: 5px">
<n-input-group>
<n-input-group-label style="width: 110px">Padded hex:</n-input-group-label>
<input-copyable
:value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')"
readonly
placeholder="Iteration count in hex will be displayed here"
/>
</n-input-group>
</n-form-item>
<p>Iteration</p>
<input-copyable
:value="String(getCounterFromTime({ now, timeStep: 30 }))"
readonly
label="Count:"
label-position="left"
label-width="90px"
label-align="right"
placeholder="Iteration count will be displayed here"
/>
<input-copyable
:value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')"
readonly
placeholder="Iteration count in hex will be displayed here"
label-position="left"
label-width="90px"
label-align="right"
label="Padded hex:"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Refresh } from '@vicons/tabler';
import { useTimestamp } from '@vueuse/core';
import { useThemeVars } from 'naive-ui';
import { useStyleStore } from '@/stores/style.store';
import InputCopyable from '@/components/InputCopyable.vue';
import { useValidation } from '@/composable/validation';
import { computedRefreshable } from '@/composable/computedRefreshable';
import { generateTOTP, buildKeyUri, generateSecret, base32toHex, getCounterFromTime } from './otp.service';
import { useQRCode } from '../qr-code-generator/useQRCode';
@@ -106,19 +113,16 @@ const { qrcode } = useQRCode({
options: { width: 210 },
});
const { attrs: secretValidationAttrs } = useValidation({
source: secret,
rules: [
{
message: 'Secret should be a base32 string',
validator: (value) => value.toUpperCase().match(/^[A-Z234567]+$/),
},
{
message: 'Please set a secret',
validator: (value) => value !== '',
},
],
});
const secretValidationRules = [
{
message: 'Secret should be a base32 string',
validator: (value: string) => value.toUpperCase().match(/^[A-Z234567]+$/),
},
{
message: 'Please set a secret',
validator: (value: string) => value !== '',
},
];
</script>
<style lang="less" scoped>

View File

@@ -8,31 +8,30 @@
<n-input-group>
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-button data-test-id="previous-otp" secondary @click.prevent="copyPrevious(tokens.previous)">{{
tokens.previous
}}</n-button>
<c-button important:h-12 data-test-id="previous-otp" @click.prevent="copyPrevious(tokens.previous)">
{{ tokens.previous }}
</c-button>
</template>
<div>{{ previousCopied ? 'Copied !' : 'Copy previous OTP' }}</div>
</n-tooltip>
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-button
tertiary
type="primary"
<c-button
data-test-id="current-otp"
class="current-otp"
important:h-12
@click.prevent="copyCurrent(tokens.current)"
>
{{ tokens.current }}
</n-button>
</c-button>
</template>
<div>{{ currentCopied ? 'Copied !' : 'Copy current OTP' }}</div>
</n-tooltip>
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-button secondary data-test-id="next-otp" @click.prevent="copyNext(tokens.next)">{{
<c-button important:h-12 data-test-id="next-otp" @click.prevent="copyNext(tokens.next)">{{
tokens.next
}}</n-button>
}}</c-button>
</template>
<div>{{ nextCopied ? 'Copied !' : 'Copy next OTP' }}</div>
</n-tooltip>

View File

@@ -0,0 +1,25 @@
import { Phone } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Phone parser and formatter',
path: '/phone-parser-and-formatter',
description:
'Parse, validate and format phone numbers. Get information about the phone number, like the country code, type, etc.',
keywords: [
'phone',
'parser',
'formatter',
'validate',
'format',
'number',
'telephone',
'mobile',
'cell',
'international',
'national',
],
component: () => import('./phone-parser-and-formatter.vue'),
icon: Phone,
createdAt: new Date('2023-05-01'),
});

View File

@@ -0,0 +1,11 @@
import { test, expect } from '@playwright/test';
test.describe('Tool - Phone parser and formatter', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/phone-parser-and-formatter');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('Phone parser and formatter - IT Tools');
});
});

View File

@@ -0,0 +1,41 @@
import type { CountryCode, NumberType } from 'libphonenumber-js/types';
import lookup from 'country-code-lookup';
export { formatTypeToHumanReadable, getFullCountryName, getDefaultCountryCode };
const typeToLabel: Record<NonNullable<NumberType>, string> = {
MOBILE: 'Mobile',
FIXED_LINE: 'Fixed line',
FIXED_LINE_OR_MOBILE: 'Fixed line or mobile',
PERSONAL_NUMBER: 'Personal number',
PREMIUM_RATE: 'Premium rate',
SHARED_COST: 'Shared cost',
TOLL_FREE: 'Toll free',
UAN: 'Universal access number',
VOICEMAIL: 'Voicemail',
VOIP: 'VoIP',
PAGER: 'Pager',
};
function formatTypeToHumanReadable(type: NumberType): string | undefined {
if (!type) return undefined;
return typeToLabel[type];
}
function getFullCountryName(countryCode: string | undefined) {
if (!countryCode) return undefined;
return lookup.byIso(countryCode)?.country;
}
function getDefaultCountryCode({
locale = window.navigator.language,
defaultCode = 'FR',
}: { locale?: string; defaultCode?: CountryCode } = {}): CountryCode {
const countryCode = locale.split('-')[1]?.toUpperCase();
if (!countryCode) return defaultCode;
return (lookup.byIso(countryCode)?.iso2 ?? defaultCode) as CountryCode;
}

View File

@@ -0,0 +1,112 @@
<template>
<div>
<n-form-item label="Default country code:">
<n-select v-model:value="defaultCountryCode" :options="countriesOptions" />
</n-form-item>
<c-input-text
v-model:value="rawPhone"
placeholder="Enter a phone number"
label="Phone number:"
:validation="validation"
mb-5
/>
<n-table v-if="parsedDetails">
<tbody>
<tr v-for="{ label, value } in parsedDetails" :key="label">
<td>
<n-text strong>{{ label }}</n-text>
</td>
<td>
<span-copyable v-if="value" :value="value"></span-copyable>
<n-text v-else depth="3" italic>Unknown</n-text>
</td>
</tr>
</tbody>
</n-table>
</div>
</template>
<script setup lang="ts">
import { withDefaultOnError } from '@/utils/defaults';
import { parsePhoneNumber, getCountries, getCountryCallingCode } from 'libphonenumber-js/max';
import { booleanToHumanReadable } from '@/utils/boolean';
import { useValidation } from '@/composable/validation';
import lookup from 'country-code-lookup';
import {
formatTypeToHumanReadable,
getFullCountryName,
getDefaultCountryCode,
} from './phone-parser-and-formatter.models';
const rawPhone = ref('');
const defaultCountryCode = ref(getDefaultCountryCode());
const validation = useValidation({
source: rawPhone,
rules: [
{
validator: (value) => value === '' || /^[0-9 +\-()]+$/.test(value),
message: 'Invalid phone number',
},
],
});
const parsedDetails = computed(() => {
if (!validation.isValid) return undefined;
const parsed = withDefaultOnError(() => parsePhoneNumber(rawPhone.value, defaultCountryCode.value), undefined);
if (!parsed) return undefined;
return [
{
label: 'Country',
value: parsed.country,
},
{
label: 'Country',
value: getFullCountryName(parsed.country),
},
{
label: 'Country calling code',
value: parsed.countryCallingCode,
},
{
label: 'Is valid?',
value: booleanToHumanReadable(parsed.isValid()),
},
{
label: 'Is possible?',
value: booleanToHumanReadable(parsed.isPossible()),
},
{
label: 'Type',
value: formatTypeToHumanReadable(parsed.getType()),
},
{
label: 'International format',
value: parsed.formatInternational(),
},
{
label: 'National format',
value: parsed.formatNational(),
},
{
label: 'E.164 format',
value: parsed.format('E.164'),
},
{
label: 'RFC3966 format',
value: parsed.format('RFC3966'),
},
];
});
const countriesOptions = getCountries().map((code) => ({
label: `${lookup.byIso(code)?.country || code} (+${getCountryCallingCode(code)})`,
value: code,
}));
</script>
<style lang="less" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<n-card>
<c-card>
<n-grid x-gap="12" y-gap="12" cols="1 600:3">
<n-gi span="2">
<n-form label-width="130" label-placement="left">
@@ -28,11 +28,11 @@
<n-gi>
<n-space justify="center" align="center" vertical>
<n-image :src="qrcode" width="200" />
<n-button secondary @click="download"> Download qr-code </n-button>
<c-button @click="download"> Download qr-code </c-button>
</n-space>
</n-gi>
</n-grid>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -1,13 +1,13 @@
<template>
<n-card>
<c-card>
<div class="port">
{{ port }}
</div>
<n-space justify="center">
<n-button secondary @click="copy"> Copy </n-button>
<n-button secondary @click="refreshPort"> Refresh </n-button>
<c-button @click="copy"> Copy </c-button>
<c-button @click="refreshPort"> Refresh </c-button>
</n-space>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -1,32 +1,28 @@
<template>
<div>
<n-card title="Arabic to roman">
<c-card title="Arabic to roman">
<n-space align="center" justify="space-between">
<n-form-item v-bind="validationNumeral">
<n-form-item v-bind="validationNumeral as any">
<n-input-number v-model:value="inputNumeral" :min="1" style="width: 200px" :show-button="false" />
</n-form-item>
<div class="result">
{{ outputRoman }}
</div>
<n-button secondary autofocus :disabled="validationNumeral.validationStatus === 'error'" @click="copyRoman">
<c-button autofocus :disabled="validationNumeral.validationStatus === 'error'" @click="copyRoman">
Copy
</n-button>
</c-button>
</n-space>
</n-card>
<br />
<n-card title="Roman to arabic">
</c-card>
<c-card title="Roman to arabic" mt-5>
<n-space align="center" justify="space-between">
<n-form-item v-bind="validationRoman">
<n-input v-model:value="inputRoman" style="width: 200px" />
</n-form-item>
<c-input-text v-model:value="inputRoman" style="width: 200px" :validation="validationRoman" />
<div class="result">
{{ outputNumeral }}
</div>
<n-button secondary autofocus :disabled="validationRoman.validationStatus === 'error'" @click="copyArabic">
Copy
</n-button>
<c-button :disabled="!validationRoman.isValid" @click="copyArabic"> Copy </c-button>
</n-space>
</n-card>
</c-card>
</div>
</template>
@@ -58,7 +54,7 @@ const { attrs: validationNumeral } = useValidation({
const inputRoman = ref('XLII');
const outputNumeral = computed(() => romanToArabic(inputRoman.value));
const { attrs: validationRoman } = useValidation({
const validationRoman = useValidation({
source: inputRoman,
rules: [
{

View File

@@ -1,11 +1,11 @@
<template>
<div style="flex: 0 0 100%">
<n-space item-style="flex: 1 1 0" style="margin: 0 auto; max-width: 600px" justify="center">
<n-form-item label="Bits :" v-bind="bitsValidationAttrs" label-placement="left" label-width="100">
<n-form-item label="Bits :" v-bind="bitsValidationAttrs as any" label-placement="left" label-width="100">
<n-input-number v-model:value="bits" min="256" max="16384" step="8" />
</n-form-item>
<n-button tertiary @click="refreshCerts">Refresh key-pair</n-button>
<c-button @click="refreshCerts">Refresh key-pair</c-button>
</n-space>
</div>

View File

@@ -14,7 +14,7 @@
</n-form-item>
<n-space justify="center">
<n-button secondary :disabled="slug.length === 0" @click="copy">Copy slug</n-button>
<c-button :disabled="slug.length === 0" @click="copy">Copy slug</c-button>
</n-space>
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More