mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-10-23 04:52:14 +00:00
Compare commits
78 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0ff853437b | ||
|
39c8f92065 | ||
|
35b5187119 | ||
|
94698cea50 | ||
|
8294cd68da | ||
|
7c9b8ac178 | ||
|
5d8f46abf8 | ||
|
35a3760771 | ||
|
4ef25887b9 | ||
|
7f229959d6 | ||
|
d3a2936979 | ||
|
5f16885923 | ||
|
ea5e7a7fc7 | ||
|
7de6c86f9e | ||
|
83da6b7ee9 | ||
|
737319edf1 | ||
|
a77a82f5a2 | ||
|
da17696293 | ||
|
164e32b442 | ||
|
49755909bd | ||
|
44d653b1f2 | ||
|
7c449f4f2d | ||
|
ab7483b5c2 | ||
|
5222bd5d04 | ||
|
cf5e4d9056 | ||
|
992f96b48a | ||
|
fcf4cfe64d | ||
|
f54223fb0a | ||
|
b38ab82d05 | ||
|
f6cd9b76d3 | ||
|
208a373fd0 | ||
|
8089c60000 | ||
|
d30cd8a9ab | ||
|
04a8e122be | ||
|
447bdf2148 | ||
|
ca7cb44389 | ||
|
e48d60b1ed | ||
|
fda0b0ca25 | ||
|
cc717bc87e | ||
|
1bc6380c6f | ||
|
02c4963531 | ||
|
129f74c371 | ||
|
0be33fb337 | ||
|
422b6eb05a | ||
|
fad4833ca2 | ||
|
531a25c1c4 | ||
|
77b5b0cab5 | ||
|
7570ad9656 | ||
|
8a9e7888de | ||
|
750a76b00f | ||
|
5f03619ab4 | ||
|
352365f012 | ||
|
4f599b6999 | ||
|
138149e6f0 | ||
|
412de23796 | ||
|
1a22d55b3c | ||
|
bb4aac6d4a | ||
|
e6953d1b67 | ||
|
a70a0f83a1 | ||
|
bdee93a9e4 | ||
|
08ce407a01 | ||
|
125a50215a | ||
|
d5738e1aef | ||
|
560fcf3f78 | ||
|
328fda65b3 | ||
|
ba87097e3d | ||
|
99383d25fc | ||
|
d1f95f5b34 | ||
|
6cd25a743e | ||
|
d2f5d3c3de | ||
|
130031c225 | ||
|
1c7257eeb0 | ||
|
214084262c | ||
|
92ce419f45 | ||
|
394d085846 | ||
|
ab53048d5f | ||
|
c3a302bc38 | ||
|
a16161cdb4 |
@@ -7,23 +7,29 @@ module.exports = {
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
'@vue/eslint-config-prettier',
|
||||
'plugin:import/recommended',
|
||||
],
|
||||
|
||||
settings: {
|
||||
'import/resolver': { typescript: { project: './tsconfig.app.json' } },
|
||||
},
|
||||
env: {
|
||||
'vue/setup-compiler-macros': true,
|
||||
},
|
||||
rules: {
|
||||
'vue/multi-word-component-names': ['off'],
|
||||
'prettier/prettier': [
|
||||
'prettier/prettier': ['error'],
|
||||
'import/no-duplicates': ['error', { considerQueryString: true }],
|
||||
'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }],
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
singleQuote: true,
|
||||
semi: true,
|
||||
tabWidth: 2,
|
||||
trailingComma: 'all',
|
||||
printWidth: 120,
|
||||
js: 'never',
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
@@ -7,32 +7,21 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup node env
|
||||
uses: actions/setup-node@v3.0.0
|
||||
- uses: actions/checkout@v3
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm i
|
||||
|
||||
- name: Run linters
|
||||
run: npm run lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Run unit test
|
||||
run: npm run test
|
||||
run: pnpm test
|
||||
|
||||
- name: Build the app
|
||||
run: npm run build
|
||||
run: pnpm build
|
||||
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120
|
||||
}
|
213
CHANGELOG.md
213
CHANGELOG.md
@@ -2,6 +2,219 @@
|
||||
|
||||
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.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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **new-tool:** chmod calculator ([35b5187](https://github.com/CorentinTh/it-tools/commit/35b518711938c2bc88f35d104bb35d9956f0c267))
|
||||
|
||||
## [2.13.0](https://github.com/CorentinTh/it-tools/compare/v2.11.0...v2.13.0) (2022-11-14)
|
||||
|
||||
|
||||
### 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)
|
||||
|
||||
|
||||
|
34
index.html
34
index.html
@@ -6,11 +6,17 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>IT Tools - Handy online tools for developers</title>
|
||||
<meta itemprop="name" content="IT Tools - Handy online tools for developers" />
|
||||
<meta name="description" content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." />
|
||||
<meta itemprop="description" content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." />
|
||||
<meta
|
||||
name="description"
|
||||
content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT."
|
||||
/>
|
||||
<meta
|
||||
itemprop="description"
|
||||
content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT."
|
||||
/>
|
||||
<link rel="author" href="/humans.txt" />
|
||||
<link rel=canonical href="https://it-tools.tech">
|
||||
|
||||
<link rel="canonical" href="https://it-tools.tech" />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
@@ -21,15 +27,23 @@
|
||||
<meta property="og:url" content="https://it-tools.tech/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="IT Tools - Handy online tools for developers" />
|
||||
<meta property="og:description" content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." />
|
||||
<meta property="og:image" content="/banner.png" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT."
|
||||
/>
|
||||
<meta property="og:image" content="https://it-tools.tech/banner.png?v=2" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="it-tools.tech" />
|
||||
<meta property="twitter:url" content="https://it-tools.tech/" />
|
||||
<meta name="twitter:site" content="@ittoolsdottech" />
|
||||
<meta name="twitter:creator" content="@cthmsst" />
|
||||
|
||||
<meta name="twitter:title" content="IT Tools - Handy online tools for developers" />
|
||||
<meta name="twitter:description" content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." />
|
||||
<meta name="twitter:image" content="/banner.png" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://it-tools.tech/banner.png?v=2" />
|
||||
<meta name="twitter:image:alt" content="IT Tools - Handy online tools for developers" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
19894
package-lock.json
generated
19894
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
71
package.json
71
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "it-tools",
|
||||
"version": "2.5.3",
|
||||
"version": "2.14.1",
|
||||
"description": "Collection of handy online tools for developers, with great UX. ",
|
||||
"keywords": [
|
||||
"productivity",
|
||||
@@ -32,59 +32,70 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@it-tools/bip39": "^0.0.4",
|
||||
"@it-tools/oggen": "^1.3.0",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"@vicons/tabler": "^0.12.0",
|
||||
"@vueuse/core": "^8.2.1",
|
||||
"@vueuse/head": "^0.7.5",
|
||||
"@vueuse/core": "^8.9.4",
|
||||
"@vueuse/head": "^0.7.13",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"change-case": "^4.1.2",
|
||||
"colord": "^2.9.2",
|
||||
"colord": "^2.9.3",
|
||||
"cron-validator": "^1.3.1",
|
||||
"cronstrue": "^2.2.0",
|
||||
"cronstrue": "^2.15.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"date-fns": "^2.28.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"figue": "^1.2.0",
|
||||
"highlight.js": "^11.5.1",
|
||||
"highlight.js": "^11.6.0",
|
||||
"json5": "^2.2.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mathjs": "^10.6.0",
|
||||
"naive-ui": "^2.28.0",
|
||||
"pinia": "^2.0.11",
|
||||
"plausible-tracker": "^0.3.5",
|
||||
"qrcode": "^1.5.0",
|
||||
"mathjs": "^10.6.4",
|
||||
"mime-types": "^2.1.35",
|
||||
"naive-ui": "^2.33.5",
|
||||
"pinia": "^2.0.23",
|
||||
"plausible-tracker": "^0.3.8",
|
||||
"qrcode": "^1.5.1",
|
||||
"randombytes": "^2.1.0",
|
||||
"sql-formatter": "^8.2.0",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^3.2.31",
|
||||
"vue-router": "^4.0.12"
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.1.0",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/jsdom": "^16.2.14",
|
||||
"@types/node": "^16.11.25",
|
||||
"@types/qrcode": "^1.4.2",
|
||||
"@types/jsdom": "^16.2.15",
|
||||
"@types/lodash": "^4.14.188",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/node": "^16.18.3",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/randombytes": "^2.0.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@vitejs/plugin-vue": "^2.2.2",
|
||||
"@vitejs/plugin-vue-jsx": "^1.3.7",
|
||||
"@typescript-eslint/parser": "^5.42.1",
|
||||
"@vitejs/plugin-vue": "^2.3.4",
|
||||
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"@vue/test-utils": "^2.0.0-rc.18",
|
||||
"@vue/test-utils": "^2.2.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"c8": "^7.11.0",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-plugin-vue": "^8.2.0",
|
||||
"c8": "^7.12.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
"jsdom": "^19.0.0",
|
||||
"less": "^4.1.2",
|
||||
"prettier": "^2.6.2",
|
||||
"standard-version": "^9.3.2",
|
||||
"less": "^4.1.3",
|
||||
"prettier": "^2.7.1",
|
||||
"standard-version": "^9.5.0",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"typescript": "~4.5.5",
|
||||
"vite": "^2.9.1",
|
||||
"vite": "^2.9.15",
|
||||
"vite-plugin-md": "^0.12.4",
|
||||
"vite-plugin-pwa": "^0.11.13",
|
||||
"vite-svg-loader": "^3.2.0",
|
||||
"vite-svg-loader": "^3.6.0",
|
||||
"vitest": "^0.13.1",
|
||||
"vue-tsc": "^0.31.4"
|
||||
"vue-tsc": "^0.31.4",
|
||||
"workbox-window": "^6.5.4"
|
||||
}
|
||||
}
|
||||
|
7378
pnpm-lock.yaml
generated
Normal file
7378
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 155 KiB |
@@ -1,6 +1,6 @@
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { mkdir, readFile, writeFile } from 'fs/promises';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const currentDirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -55,7 +55,7 @@ export const tool = defineTool({
|
||||
keywords: ['${toolName.split('-').join("', '")}'],
|
||||
component: () => import('./${toolName}.vue'),
|
||||
icon: ArrowsShuffle,
|
||||
};
|
||||
});
|
||||
`,
|
||||
);
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { layouts } from './layouts';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, RouterView } from 'vue-router';
|
||||
import { darkThemeOverrides, lightThemeOverrides } from './themes';
|
||||
import { darkTheme, NGlobalStyle, NMessageProvider } from 'naive-ui';
|
||||
import { darkThemeOverrides, lightThemeOverrides } from './themes';
|
||||
import { layouts } from './layouts';
|
||||
import { useStyleStore } from './stores/style.store';
|
||||
|
||||
const route = useRoute();
|
||||
|
59
src/components/ColoredCard.vue
Normal file
59
src/components/ColoredCard.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<n-card class="colored-card">
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-icon class="icon" size="40" :component="icon" />
|
||||
</n-space>
|
||||
<n-h3 class="title">
|
||||
<n-ellipsis>{{ title }}</n-ellipsis>
|
||||
</n-h3>
|
||||
|
||||
<div class="description">
|
||||
<n-ellipsis :line-clamp="2" :tooltip="false">
|
||||
<slot />
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRefs, type Component } from 'vue';
|
||||
|
||||
const props = defineProps<{ icon: Component; title: string }>();
|
||||
const { icon, title } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.colored-card {
|
||||
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;
|
||||
border: none;
|
||||
|
||||
.icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
opacity: 0.8;
|
||||
|
||||
margin: 5px 0;
|
||||
|
||||
::v-deep(a) {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
transition: color ease 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: rgb(20, 20, 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -9,6 +9,7 @@
|
||||
href="https://github.com/CorentinTh/it-tools"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT-Tools' github repository"
|
||||
>
|
||||
<n-icon size="25" :component="BrandGithub" />
|
||||
</n-button>
|
||||
@@ -26,17 +27,18 @@
|
||||
href="https://twitter.com/ittoolsdottech"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT Tools' twitter account"
|
||||
>
|
||||
<n-icon size="25" :component="BrandTwitter" />
|
||||
</n-button>
|
||||
</template>
|
||||
Creator twitter
|
||||
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="Home" @click="navigate">
|
||||
<n-button tag="a" :href="href" circle quaternary size="large" aria-label="About" @click="navigate">
|
||||
<n-icon size="25" :component="InfoCircle" />
|
||||
</n-button>
|
||||
</template>
|
||||
@@ -45,7 +47,7 @@
|
||||
</router-link>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button size="large" circle quaternary @click="isDarkTheme = !isDarkTheme">
|
||||
<n-button size="large" circle quaternary aria-label="Toggle dark/light mode" @click="isDarkTheme = !isDarkTheme">
|
||||
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
|
||||
<n-icon v-else size="25" :component="Moon" />
|
||||
</n-button>
|
||||
@@ -57,8 +59,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler';
|
||||
import { toRefs } from 'vue';
|
||||
import { BrandGithub, BrandTwitter, Moon, Sun, InfoCircle } from '@vicons/tabler';
|
||||
|
||||
const styleStore = useStyleStore();
|
||||
const { isDarkTheme } = toRefs(styleStore);
|
||||
|
@@ -1,8 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { SearchRound } from '@vicons/material';
|
||||
import { computed, ref } from 'vue';
|
||||
import { deburr } from 'lodash';
|
||||
import { tools } from '@/tools';
|
||||
import { SearchRound } from '@vicons/material';
|
||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
||||
import { deburr } from 'lodash';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -28,6 +29,21 @@ function onSelect(path: string) {
|
||||
router.push(path);
|
||||
queryString.value = '';
|
||||
}
|
||||
|
||||
const focusTarget = ref();
|
||||
|
||||
const keys = useMagicKeys({
|
||||
passive: false,
|
||||
onEventFired(e) {
|
||||
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
whenever(keys.ctrl_k, () => {
|
||||
focusTarget.value.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -40,9 +56,10 @@ function onSelect(path: string) {
|
||||
>
|
||||
<template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
|
||||
<n-input
|
||||
ref="focusTarget"
|
||||
round
|
||||
clearable
|
||||
placeholder="Search a tool..."
|
||||
placeholder="Search a tool... [Ctrl + K]"
|
||||
:value="slotValue"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
|
100
src/components/TextareaCopyable.vue
Normal file
100
src/components/TextareaCopyable.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div style="overflow-x: hidden; width: 100%">
|
||||
<n-card class="result-card">
|
||||
<n-scrollbar
|
||||
x-scrollable
|
||||
trigger="none"
|
||||
:style="height ? `min-height: ${height - 40 /* card padding */ + 10 /* negative margin compensation */}px` : ''"
|
||||
>
|
||||
<n-config-provider :hljs="hljs">
|
||||
<n-code :code="value" :language="language" :trim="false" />
|
||||
</n-config-provider>
|
||||
</n-scrollbar>
|
||||
<n-tooltip v-if="value" trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="copy-button" :class="[copyPlacement]">
|
||||
<n-button secondary circle size="large" @click="onCopyClicked">
|
||||
<n-icon size="22" :component="Copy" />
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
<span>{{ tooltipText }}</span>
|
||||
</n-tooltip>
|
||||
</n-card>
|
||||
<n-space v-if="copyPlacement === 'outside'" justify="center" style="margin-top: 15px">
|
||||
<n-button secondary @click="onCopyClicked"> {{ tooltipText }} </n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Copy } from '@vicons/tabler';
|
||||
import { useClipboard, useElementSize } from '@vueuse/core';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import jsonHljs from 'highlight.js/lib/languages/json';
|
||||
import sqlHljs from 'highlight.js/lib/languages/sql';
|
||||
import xmlHljs from 'highlight.js/lib/languages/xml';
|
||||
import { ref, toRefs } from 'vue';
|
||||
|
||||
hljs.registerLanguage('sql', sqlHljs);
|
||||
hljs.registerLanguage('json', jsonHljs);
|
||||
hljs.registerLanguage('html', xmlHljs);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: string;
|
||||
followHeightOf?: HTMLElement | null;
|
||||
language?: string;
|
||||
copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none';
|
||||
copyMessage?: string;
|
||||
}>(),
|
||||
{
|
||||
followHeightOf: null,
|
||||
language: 'txt',
|
||||
copyPlacement: 'top-right',
|
||||
copyMessage: 'Copy to clipboard',
|
||||
},
|
||||
);
|
||||
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
|
||||
const { height } = followHeightOf ? useElementSize(followHeightOf) : { height: ref(null) };
|
||||
|
||||
const { copy } = useClipboard({ source: value });
|
||||
const tooltipText = ref(copyMessage.value);
|
||||
|
||||
function onCopyClicked() {
|
||||
copy();
|
||||
tooltipText.value = 'Copied !';
|
||||
|
||||
setTimeout(() => {
|
||||
tooltipText.value = copyMessage.value;
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
::v-deep(.n-scrollbar) {
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
.result-card {
|
||||
position: relative;
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
|
||||
&.top-right {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
&.bottom-right {
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
&.outside,
|
||||
&.none {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -49,7 +49,8 @@ a {
|
||||
}
|
||||
|
||||
.icon {
|
||||
opacity: 0.7;
|
||||
opacity: 0.6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -57,7 +58,8 @@ a {
|
||||
}
|
||||
|
||||
.description {
|
||||
opacity: 0.7;
|
||||
opacity: 0.6;
|
||||
color: #ffffff;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,36 @@
|
||||
import { extension as getExtensionFromMime } from 'mime-types';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
export function useDownloadFileFromBase64({ source, filename = 'file' }: { source: Ref<string>; filename?: string }) {
|
||||
function getFileExtensionFromBase64({
|
||||
base64String,
|
||||
defaultExtension = 'txt',
|
||||
}: {
|
||||
base64String: string;
|
||||
defaultExtension?: string;
|
||||
}) {
|
||||
const hasMimeType = base64String.match(/data:(.*?);base64/i);
|
||||
|
||||
if (hasMimeType) {
|
||||
return getExtensionFromMime(hasMimeType[1]) || defaultExtension;
|
||||
}
|
||||
|
||||
return defaultExtension;
|
||||
}
|
||||
|
||||
export function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
|
||||
return {
|
||||
download() {
|
||||
const base64String = source.value;
|
||||
|
||||
if (base64String === '') {
|
||||
throw new Error('Base64 string is empty');
|
||||
}
|
||||
|
||||
const cleanFileName = filename ?? `file.${getFileExtensionFromBase64({ base64String })}`;
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = source.value;
|
||||
a.download = filename;
|
||||
a.href = base64String;
|
||||
a.download = cleanFileName;
|
||||
a.click();
|
||||
},
|
||||
};
|
||||
|
29
src/composable/validation.test.ts
Normal file
29
src/composable/validation.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isFalsyOrHasThrown } from './validation';
|
||||
|
||||
describe('useValidation', () => {
|
||||
describe('isFalsyOrHasThrown', () => {
|
||||
it('should return true if the callback return nil, false or throw', () => {
|
||||
expect(isFalsyOrHasThrown(() => false)).toBe(true);
|
||||
expect(isFalsyOrHasThrown(() => null)).toBe(true);
|
||||
expect(isFalsyOrHasThrown(() => undefined)).toBe(true);
|
||||
expect(isFalsyOrHasThrown(() => {})).toBe(true);
|
||||
expect(
|
||||
isFalsyOrHasThrown(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for any truthy values and empty string and 0 values', () => {
|
||||
expect(isFalsyOrHasThrown(() => true)).toBe(false);
|
||||
expect(isFalsyOrHasThrown(() => 'string')).toBe(false);
|
||||
expect(isFalsyOrHasThrown(() => 1)).toBe(false);
|
||||
expect(isFalsyOrHasThrown(() => 0)).toBe(false);
|
||||
expect(isFalsyOrHasThrown(() => '')).toBe(false);
|
||||
expect(isFalsyOrHasThrown(() => [])).toBe(false);
|
||||
expect(isFalsyOrHasThrown(() => ({}))).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,38 +1,65 @@
|
||||
import _ from 'lodash';
|
||||
import { reactive, watch, type Ref } from 'vue';
|
||||
|
||||
type UseValidationRule<T> = {
|
||||
validator: (value: T) => boolean;
|
||||
message: string;
|
||||
};
|
||||
type ValidatorReturnType = unknown;
|
||||
|
||||
function isFalsyOrHasThrown(cb: () => boolean) {
|
||||
interface UseValidationRule<T> {
|
||||
validator: (value: T) => ValidatorReturnType;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
|
||||
try {
|
||||
return !cb();
|
||||
const returnValue = cb();
|
||||
|
||||
if (_.isNil(returnValue)) return true;
|
||||
|
||||
return returnValue === false;
|
||||
} catch (_) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
type ValidationAttrs = {
|
||||
feedback: string;
|
||||
validationStatus: string | undefined;
|
||||
};
|
||||
|
||||
export function useValidation<T>({ source, rules }: { source: Ref<T>; rules: UseValidationRule<T>[] }) {
|
||||
const state = reactive<{
|
||||
message: string;
|
||||
status: undefined | 'error';
|
||||
isValid: boolean;
|
||||
attrs: ValidationAttrs;
|
||||
}>({
|
||||
message: '',
|
||||
status: undefined,
|
||||
isValid: false,
|
||||
attrs: {
|
||||
validationStatus: undefined,
|
||||
feedback: '',
|
||||
},
|
||||
});
|
||||
|
||||
watch([source], () => {
|
||||
state.message = '';
|
||||
state.status = undefined;
|
||||
watch(
|
||||
[source],
|
||||
() => {
|
||||
state.message = '';
|
||||
state.status = undefined;
|
||||
|
||||
for (const rule of rules) {
|
||||
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
|
||||
state.message = rule.message;
|
||||
state.status = 'error';
|
||||
for (const rule of rules) {
|
||||
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
|
||||
state.message = rule.message;
|
||||
state.status = 'error';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.isValid = state.status !== 'error';
|
||||
state.attrs.feedback = state.message;
|
||||
state.attrs.validationStatus = state.status;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
@@ -4,14 +4,14 @@ import { h } from 'vue';
|
||||
import { RouterLink, useRoute } from 'vue-router';
|
||||
import { Heart, Menu2, Home2 } from '@vicons/tabler';
|
||||
import { toolsByCategory } from '@/tools';
|
||||
import SearchBar from '../components/SearchBar.vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
import HeroGradient from '../assets/hero-gradient.svg?component';
|
||||
import MenuLayout from '../components/MenuLayout.vue';
|
||||
import NavbarButtons from '../components/NavbarButtons.vue';
|
||||
import { config } from '@/config';
|
||||
import MenuIconItem from '@/components/MenuIconItem.vue';
|
||||
import type { ITool } from '@/tools/tool';
|
||||
import SearchBar from '../components/SearchBar.vue';
|
||||
import HeroGradient from '../assets/hero-gradient.svg?component';
|
||||
import MenuLayout from '../components/MenuLayout.vue';
|
||||
import NavbarButtons from '../components/NavbarButtons.vue';
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
const route = useRoute();
|
||||
@@ -107,7 +107,7 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
|
||||
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
|
||||
circle
|
||||
quaternary
|
||||
aria-label="Toogle menu"
|
||||
aria-label="Toggle menu"
|
||||
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
|
||||
>
|
||||
<n-icon size="25" :component="Menu2" />
|
||||
@@ -134,23 +134,24 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
|
||||
|
||||
<search-bar />
|
||||
|
||||
<navbar-buttons v-if="!styleStore.isSmallScreen" />
|
||||
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button
|
||||
round
|
||||
type="primary"
|
||||
tag="a"
|
||||
href="https://github.com/sponsors/CorentinTh"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<n-icon v-if="!styleStore.isSmallScreen" :component="Heart" style="margin-right: 5px" />
|
||||
Sponsor
|
||||
Buy me a coffee
|
||||
<n-icon v-if="!styleStore.isSmallScreen" :component="Heart" style="margin-left: 5px" />
|
||||
</n-button>
|
||||
</template>
|
||||
❤ Support IT Tools developement !
|
||||
❤ Support IT Tools development !
|
||||
</n-tooltip>
|
||||
|
||||
<navbar-buttons v-if="!styleStore.isSmallScreen" />
|
||||
</div>
|
||||
<slot />
|
||||
</template>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import BaseLayout from './base.layout.vue';
|
||||
import { useHead } from '@vueuse/head';
|
||||
import type { HeadObject } from '@vueuse/head';
|
||||
import { computed } from 'vue';
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import BaseLayout from './base.layout.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const theme = useThemeVars();
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { createHead } from '@vueuse/head';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
import { plausible } from './plugins/plausible.plugin';
|
||||
|
||||
|
@@ -6,7 +6,7 @@ useHead({ title: 'Page not found - IT Tools' });
|
||||
|
||||
<template>
|
||||
<div class="e404-wrapper">
|
||||
<n-result status="404" title="404 Not Found" description="Sorry, this page does not seem to extist">
|
||||
<n-result status="404" title="404 Not Found" description="Sorry, this page does not seem to exist">
|
||||
<template #footer>
|
||||
<router-link to="/" #="{ navigate, href }" custom>
|
||||
<n-button tag="a" :href="href" secondary type="success" @click="navigate"> Back home </n-button>
|
||||
|
@@ -11,11 +11,11 @@ useHead({ title: 'About - IT Tools' });
|
||||
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 usefull, please fell free to
|
||||
share it to people you think may find it usefull too and dont forget to pin it in your shortcut bar !
|
||||
>, 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 opensource (under the MIT license) and free, and will always be, but it cost me money to host and
|
||||
IT Tools is open-source (under the MIT license) and free, and will always be, but it 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
|
||||
@@ -33,7 +33,7 @@ useHead({ title: 'About - IT Tools' });
|
||||
<n-h2>Technologies</n-h2>
|
||||
<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 opensource libraries are used in some tools, you may find the complete list in the
|
||||
by Vercel. Third party open-source libraries are used in some tools, you may find the complete list in the
|
||||
<n-button
|
||||
type="primary"
|
||||
tag="a"
|
||||
@@ -47,7 +47,7 @@ useHead({ title: 'About - IT Tools' });
|
||||
file of the repository.
|
||||
</n-p>
|
||||
|
||||
<n-h2>Found a bug ? A tool is missing ?</n-h2>
|
||||
<n-h2>Found a bug? A tool is missing?</n-h2>
|
||||
<n-p>
|
||||
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
|
||||
|
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { toolsWithCategory } from '@/tools';
|
||||
import ToolCard from '../components/ToolCard.vue';
|
||||
import { Heart } from '@vicons/tabler';
|
||||
import { useHead } from '@vueuse/head';
|
||||
import ColoredCard from '../components/ColoredCard.vue';
|
||||
import ToolCard from '../components/ToolCard.vue';
|
||||
|
||||
useHead({ title: 'IT Tools - Handy online tools for developers' });
|
||||
</script>
|
||||
@@ -9,13 +11,28 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||
<n-gi
|
||||
v-for="tool in [
|
||||
...toolsWithCategory.filter(({ isNew }) => isNew),
|
||||
...toolsWithCategory.filter(({ isNew }) => !isNew),
|
||||
]"
|
||||
:key="tool.name"
|
||||
>
|
||||
<n-gi>
|
||||
<colored-card title="You like it-tools?" :icon="Heart">
|
||||
Give us a star on
|
||||
<a
|
||||
href="https://github.com/CorentinTh/it-tools"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT-Tools' github repository"
|
||||
>github</a
|
||||
>
|
||||
or follow us on
|
||||
<a
|
||||
href="https://twitter.com/ittoolsdottech"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
aria-label="IT-Tools' twitter account"
|
||||
>twitter</a
|
||||
>! Thank you
|
||||
<n-icon :component="Heart" />
|
||||
</colored-card>
|
||||
</n-gi>
|
||||
<n-gi v-for="tool in toolsWithCategory" :key="tool.name">
|
||||
<tool-card :tool="tool" />
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
@@ -1,59 +1,65 @@
|
||||
import {
|
||||
create,
|
||||
NAlert,
|
||||
NAutoComplete,
|
||||
NButton,
|
||||
NConfigProvider,
|
||||
NCard,
|
||||
NInput,
|
||||
NColorPicker,
|
||||
NInputNumber,
|
||||
NSpace,
|
||||
NH1,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NTimePicker,
|
||||
NText,
|
||||
NIcon,
|
||||
NSwitch,
|
||||
NCollapseTransition,
|
||||
NGrid,
|
||||
NGridItem,
|
||||
NPopconfirm,
|
||||
NSlider,
|
||||
NCode,
|
||||
NCollapse,
|
||||
NCollapseItem,
|
||||
NProgress,
|
||||
NAutoComplete,
|
||||
NSelect,
|
||||
NUpload,
|
||||
NEmpty,
|
||||
NModal,
|
||||
NTooltip,
|
||||
NAlert,
|
||||
NP,
|
||||
NH2,
|
||||
NCollapseTransition,
|
||||
NColorPicker,
|
||||
NConfigProvider,
|
||||
NDatePicker,
|
||||
NDivider,
|
||||
NDropdown,
|
||||
NDynamicInput,
|
||||
NEllipsis,
|
||||
NEmpty,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NGradientText,
|
||||
NGrid,
|
||||
NGridItem,
|
||||
NH1,
|
||||
NH2,
|
||||
NH3,
|
||||
NIcon,
|
||||
NImage,
|
||||
NInput,
|
||||
NInputGroup,
|
||||
NInputGroupLabel,
|
||||
NInputNumber,
|
||||
NLayout,
|
||||
NLayoutSider,
|
||||
NMenu,
|
||||
NMessageProvider,
|
||||
NModal,
|
||||
NP,
|
||||
NPageHeader,
|
||||
NPopconfirm,
|
||||
NProgress,
|
||||
NResult,
|
||||
NH3,
|
||||
NEllipsis,
|
||||
NTag,
|
||||
NInputGroup,
|
||||
NInputGroupLabel,
|
||||
NDivider,
|
||||
NStatistic,
|
||||
NTable,
|
||||
NUploadDragger,
|
||||
NImage,
|
||||
NScrollbar,
|
||||
NGradientText,
|
||||
NCode,
|
||||
NSelect,
|
||||
NSlider,
|
||||
NSpace,
|
||||
NStatistic,
|
||||
NSwitch,
|
||||
NTable,
|
||||
NTag,
|
||||
NText,
|
||||
NTimePicker,
|
||||
NTooltip,
|
||||
NUpload,
|
||||
NUploadDragger,
|
||||
NCheckbox,
|
||||
} from 'naive-ui';
|
||||
|
||||
const components = [
|
||||
NCheckbox,
|
||||
NDynamicInput,
|
||||
NDatePicker,
|
||||
NCode,
|
||||
NGradientText,
|
||||
NScrollbar,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { layouts } from './layouts/index';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { layouts } from './layouts/index';
|
||||
import HomePage from './pages/Home.page.vue';
|
||||
import NotFound from './pages/404.page.vue';
|
||||
import { tools } from './tools';
|
||||
|
@@ -30,4 +30,9 @@ export const darkThemeOverrides: GlobalThemeOverrides = {
|
||||
color: '#1e1e1e',
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
|
||||
Table: {
|
||||
tdColor: '#1e1e1e',
|
||||
thColor: '#353535',
|
||||
},
|
||||
};
|
||||
|
@@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<n-card title="Text to base64">
|
||||
<n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." />
|
||||
<n-input :value="textBase64" type="textarea" readonly />
|
||||
<n-card title="Base64 to file">
|
||||
<n-form-item
|
||||
:feedback="base64InputValidation.message"
|
||||
:validation-status="base64InputValidation.status"
|
||||
:show-label="false"
|
||||
>
|
||||
<n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" />
|
||||
</n-form-item>
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="copyTextBase64()"> Copy </n-button>
|
||||
<n-button :disabled="base64Input === '' || !base64InputValidation.isValid" secondary @click="downloadFile()">
|
||||
Download file
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
@@ -17,7 +24,7 @@
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
|
||||
<n-input :value="fileBase64" type="textarea" readonly />
|
||||
<n-input :value="fileBase64" type="textarea" readonly placeholder="File in base64 will be here" />
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="copyFileBase64()"> Copy </n-button>
|
||||
</n-space>
|
||||
@@ -26,14 +33,35 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useBase64 } from '@vueuse/core';
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { isValidBase64 } from '@/utils/base64';
|
||||
import { Upload } from '@vicons/tabler';
|
||||
import { ref, type Ref } from 'vue';
|
||||
import { useBase64 } from '@vueuse/core';
|
||||
import type { UploadFileInfo } from 'naive-ui';
|
||||
import { ref, type Ref } from 'vue';
|
||||
|
||||
const textInput = ref('');
|
||||
const { base64: textBase64 } = useBase64(textInput);
|
||||
const { copy: copyTextBase64 } = useCopy({ source: textBase64, text: 'Base64 string copied to the clipboard' });
|
||||
const base64Input = ref('');
|
||||
const { download } = useDownloadFileFromBase64({ source: base64Input });
|
||||
const base64InputValidation = useValidation({
|
||||
source: base64Input,
|
||||
rules: [
|
||||
{
|
||||
message: 'Invalid base 64 string',
|
||||
validator: (value) => isValidBase64(value.trim()),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function downloadFile() {
|
||||
if (!base64InputValidation.isValid) return;
|
||||
|
||||
try {
|
||||
download();
|
||||
} catch (_) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
const fileList = ref();
|
||||
const fileInput = ref() as Ref<File>;
|
@@ -2,11 +2,10 @@ import { FileDigit } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Base64 converter',
|
||||
path: '/base64-converter',
|
||||
name: 'Base64 file converter',
|
||||
path: '/base64-file-converter',
|
||||
description: "Convert string, files or images into a it's base64 representation.",
|
||||
keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'],
|
||||
component: () => import('./base64-converter.vue'),
|
||||
component: () => import('./base64-file-converter.vue'),
|
||||
icon: FileDigit,
|
||||
redirectFrom: ['/file-to-base64', '/base64-string-converter'],
|
||||
});
|
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<n-card title="String to base64">
|
||||
<n-form-item label="String to encode">
|
||||
<n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." rows="5" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="Base64 of string">
|
||||
<n-input
|
||||
:value="base64Output"
|
||||
type="textarea"
|
||||
readonly
|
||||
placeholder="The base64 encoding of your string will be here"
|
||||
rows="5"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="copyTextBase64()"> Copy base64 </n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card title="Base64 to string">
|
||||
<n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs">
|
||||
<n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="Decoded string">
|
||||
<n-input :value="textOutput" type="textarea" readonly placeholder="The decoded string will be here" rows="5" />
|
||||
</n-form-item>
|
||||
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="copyText()"> Copy decoded string </n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const textInput = ref('');
|
||||
const base64Output = computed(() => textToBase64(textInput.value));
|
||||
const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' });
|
||||
|
||||
const base64Input = ref('');
|
||||
const textOutput = computed(() => withDefaultOnError(() => base64ToText(base64Input.value.trim()), ''));
|
||||
const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' });
|
||||
const b64Validation = useValidation({
|
||||
source: base64Input,
|
||||
rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
12
src/tools/base64-string-converter/index.ts
Normal file
12
src/tools/base64-string-converter/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { FileDigit } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Base64 string encoder/decoder',
|
||||
path: '/base64-string-converter',
|
||||
description: 'Simply encode and decode string into a their base64 representation.',
|
||||
keywords: ['base64', 'converter', 'conversion', 'web', 'data', 'format', 'atob', 'btoa'],
|
||||
component: () => import('./base64-string-converter.vue'),
|
||||
icon: FileDigit,
|
||||
redirectFrom: ['/file-to-base64', '/base64-converter'],
|
||||
});
|
49
src/tools/basic-auth-generator/basic-auth-generator.vue
Normal file
49
src/tools/basic-auth-generator/basic-auth-generator.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-form-item label="Username">
|
||||
<n-input v-model:value="username" placeholder="Your username..." clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="Password">
|
||||
<n-input
|
||||
v-model:value="password"
|
||||
placeholder="Your password..."
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<br />
|
||||
<n-card>
|
||||
<n-statistic label="Authorization header:" class="header">
|
||||
<n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none">
|
||||
{{ header }}
|
||||
</n-scrollbar>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
<br />
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="copy">Copy header</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { textToBase64 } from '@/utils/base64';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const header = computed(() => `Authorization: Basic ${textToBase64(`${username.value}:${password.value}`)}`);
|
||||
|
||||
const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
::v-deep(.n-statistic-value__content) {
|
||||
font-family: monospace;
|
||||
font-size: 17px !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
21
src/tools/basic-auth-generator/index.ts
Normal file
21
src/tools/basic-auth-generator/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { PasswordRound } from '@vicons/material';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Basic auth generator',
|
||||
path: '/basic-auth-generator',
|
||||
description: 'Generate a base64 basic auth header from an username and a password.',
|
||||
keywords: [
|
||||
'basic',
|
||||
'auth',
|
||||
'generator',
|
||||
'username',
|
||||
'password',
|
||||
'base64',
|
||||
'authentication',
|
||||
'header',
|
||||
'authorization',
|
||||
],
|
||||
component: () => import('./basic-auth-generator.vue'),
|
||||
icon: PasswordRound,
|
||||
});
|
@@ -59,24 +59,26 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { isNotThrowing } from '@/utils/boolean';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import {
|
||||
entropyToMnemonic,
|
||||
englishWordList,
|
||||
chineseSimplifiedWordList,
|
||||
chineseTraditionalWordList,
|
||||
czechWordList,
|
||||
englishWordList,
|
||||
entropyToMnemonic,
|
||||
frenchWordList,
|
||||
generateEntropy,
|
||||
italianWordList,
|
||||
japaneseWordList,
|
||||
koreanWordList,
|
||||
mnemonicToEntropy,
|
||||
portugueseWordList,
|
||||
spanishWordList,
|
||||
generateEntropy,
|
||||
mnemonicToEntropy,
|
||||
} from '@it-tools/bip39';
|
||||
import { Copy, Refresh } from '@vicons/tabler';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const languages = {
|
||||
English: englishWordList,
|
||||
@@ -97,20 +99,11 @@ const passphraseInput = ref('');
|
||||
const language = ref<keyof typeof languages>('English');
|
||||
const passphrase = computed({
|
||||
get() {
|
||||
try {
|
||||
return entropyToMnemonic(entropy.value, languages[language.value]);
|
||||
} catch (_) {
|
||||
return passphraseInput.value;
|
||||
}
|
||||
return withDefaultOnError(() => entropyToMnemonic(entropy.value, languages[language.value]), passphraseInput.value);
|
||||
},
|
||||
set(value: string) {
|
||||
passphraseInput.value = value;
|
||||
|
||||
try {
|
||||
entropy.value = mnemonicToEntropy(value, languages[language.value]);
|
||||
} catch (_) {
|
||||
entropy.value = '';
|
||||
}
|
||||
entropy.value = withDefaultOnError(() => mnemonicToEntropy(value, languages[language.value]), '');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -123,7 +116,7 @@ const entropyValidation = useValidation({
|
||||
},
|
||||
{
|
||||
validator: (value) => /^[a-fA-F0-9]*$/.test(value),
|
||||
message: 'Entropy should an hexadecimal number',
|
||||
message: 'Entropy should be an hexadecimal string',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -132,14 +125,7 @@ const mnemonicValidation = useValidation({
|
||||
source: passphrase,
|
||||
rules: [
|
||||
{
|
||||
validator: (value) => {
|
||||
try {
|
||||
mnemonicToEntropy(value);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
validator: (value) => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
|
||||
message: 'Invalid mnemonic',
|
||||
},
|
||||
],
|
||||
|
@@ -46,8 +46,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
|
||||
import {
|
||||
camelCase,
|
||||
capitalCase,
|
||||
@@ -61,6 +59,7 @@ import {
|
||||
sentenceCase,
|
||||
snakeCase,
|
||||
} from 'change-case';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
|
||||
const input = ref('lorem ipsum dolor sit amet');
|
||||
</script>
|
||||
|
68
src/tools/chmod-calculator/chmod-calculator.service.test.ts
Normal file
68
src/tools/chmod-calculator/chmod-calculator.service.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
|
||||
|
||||
describe('chmod-calculator', () => {
|
||||
describe('computeChmodOctalRepresentation', () => {
|
||||
it('get the octal representation from permissions', () => {
|
||||
expect(
|
||||
computeChmodOctalRepresentation({
|
||||
permissions: {
|
||||
owner: { read: true, write: true, execute: true },
|
||||
group: { read: true, write: true, execute: true },
|
||||
public: { read: true, write: true, execute: true },
|
||||
},
|
||||
}),
|
||||
).to.eql('777');
|
||||
|
||||
expect(
|
||||
computeChmodOctalRepresentation({
|
||||
permissions: {
|
||||
owner: { read: false, write: false, execute: false },
|
||||
group: { read: false, write: false, execute: false },
|
||||
public: { read: false, write: false, execute: false },
|
||||
},
|
||||
}),
|
||||
).to.eql('000');
|
||||
|
||||
expect(
|
||||
computeChmodOctalRepresentation({
|
||||
permissions: {
|
||||
owner: { read: false, write: true, execute: false },
|
||||
group: { read: false, write: true, execute: true },
|
||||
public: { read: true, write: false, execute: true },
|
||||
},
|
||||
}),
|
||||
).to.eql('235');
|
||||
|
||||
expect(
|
||||
computeChmodOctalRepresentation({
|
||||
permissions: {
|
||||
owner: { read: true, write: false, execute: false },
|
||||
group: { read: false, write: true, execute: false },
|
||||
public: { read: false, write: false, execute: true },
|
||||
},
|
||||
}),
|
||||
).to.eql('421');
|
||||
|
||||
expect(
|
||||
computeChmodOctalRepresentation({
|
||||
permissions: {
|
||||
owner: { read: false, write: false, execute: true },
|
||||
group: { read: false, write: true, execute: false },
|
||||
public: { read: true, write: false, execute: false },
|
||||
},
|
||||
}),
|
||||
).to.eql('124');
|
||||
|
||||
expect(
|
||||
computeChmodOctalRepresentation({
|
||||
permissions: {
|
||||
owner: { read: false, write: true, execute: false },
|
||||
group: { read: false, write: true, execute: false },
|
||||
public: { read: false, write: true, execute: false },
|
||||
},
|
||||
}),
|
||||
).to.eql('222');
|
||||
});
|
||||
});
|
||||
});
|
17
src/tools/chmod-calculator/chmod-calculator.service.ts
Normal file
17
src/tools/chmod-calculator/chmod-calculator.service.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import _ from 'lodash';
|
||||
import type { GroupPermissions, Permissions } from './chmod-calculator.types';
|
||||
|
||||
export { computeChmodOctalRepresentation };
|
||||
|
||||
function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string {
|
||||
const permissionValue = { read: 4, write: 2, execute: 1 };
|
||||
|
||||
const getGroupPermissionValue = (permission: GroupPermissions) =>
|
||||
_.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, 0) : 0), 0);
|
||||
|
||||
return [
|
||||
getGroupPermissionValue(permissions.owner),
|
||||
getGroupPermissionValue(permissions.group),
|
||||
getGroupPermissionValue(permissions.public),
|
||||
].join('');
|
||||
}
|
10
src/tools/chmod-calculator/chmod-calculator.types.ts
Normal file
10
src/tools/chmod-calculator/chmod-calculator.types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type Scope = 'read' | 'write' | 'execute';
|
||||
export type Group = 'owner' | 'group' | 'public';
|
||||
|
||||
export type GroupPermissions = {
|
||||
[k in Scope]: boolean;
|
||||
};
|
||||
|
||||
export type Permissions = {
|
||||
[k in Group]: GroupPermissions;
|
||||
};
|
83
src/tools/chmod-calculator/chmod-calculator.vue
Normal file
83
src/tools/chmod-calculator/chmod-calculator.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" scope="col"></th>
|
||||
<th class="text-center" scope="col">Owner (u)</th>
|
||||
<th class="text-center" scope="col">Group (g)</th>
|
||||
<th class="text-center" scope="col">Public (o)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="{ scope, title } of scopes" :key="scope">
|
||||
<td class="line-header">{{ title }}</td>
|
||||
<td v-for="group of groups" :key="group" class="text-center">
|
||||
<!-- <n-switch v-model:value="permissions[group][scope]" /> -->
|
||||
<n-checkbox v-model:checked="permissions[group][scope]" size="large" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
|
||||
<div class="octal-result">
|
||||
{{ octal }}
|
||||
</div>
|
||||
|
||||
<input-copyable :value="`chmod ${octal} path`" readonly style="margin-bottom: 5px" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import { computed, ref } from 'vue';
|
||||
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
|
||||
import type { Group, Scope } from './chmod-calculator.types';
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
|
||||
const scopes: { scope: Scope; title: string }[] = [
|
||||
{ scope: 'read', title: 'Read (4)' },
|
||||
{ scope: 'write', title: 'Write (2)' },
|
||||
{ scope: 'execute', title: 'Execute (1)' },
|
||||
];
|
||||
const groups: Group[] = ['owner', 'group', 'public'];
|
||||
|
||||
const permissions = ref({
|
||||
owner: { read: false, write: false, execute: false },
|
||||
group: { read: false, write: false, execute: false },
|
||||
public: { read: false, write: false, execute: false },
|
||||
});
|
||||
|
||||
const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.octal-result {
|
||||
text-align: center;
|
||||
font-size: 50px;
|
||||
font-family: monospace;
|
||||
color: v-bind('themeVars.primaryColor');
|
||||
margin: 20px 0;
|
||||
}
|
||||
.permission-table {
|
||||
td,
|
||||
th {
|
||||
padding: 15px;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.line-header {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
max-width: 80px;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
22
src/tools/chmod-calculator/index.ts
Normal file
22
src/tools/chmod-calculator/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { FileInvoice } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Chmod calculator',
|
||||
path: '/chmod-calculator',
|
||||
description: 'Compute your chmod permissions and commands with this online chmod calculator.',
|
||||
keywords: [
|
||||
'chmod',
|
||||
'calculator',
|
||||
'file',
|
||||
'permission',
|
||||
'files',
|
||||
'directory',
|
||||
'folder',
|
||||
'recursive',
|
||||
'generator',
|
||||
'octal',
|
||||
],
|
||||
component: () => import('./chmod-calculator.vue'),
|
||||
icon: FileInvoice,
|
||||
});
|
13
src/tools/chronometer/chronometer.service.test.ts
Normal file
13
src/tools/chronometer/chronometer.service.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatMs } from './chronometer.service';
|
||||
|
||||
describe('chronometer', () => {
|
||||
describe('formatChronometerTime', () => {
|
||||
it('format the elapsed time', () => {
|
||||
expect(formatMs(0)).toEqual('00:00.000');
|
||||
expect(formatMs(1)).toEqual('00:00.001');
|
||||
expect(formatMs(123456)).toEqual('02:03.456');
|
||||
expect(formatMs(12345600)).toEqual('03:25:45.600');
|
||||
});
|
||||
});
|
||||
});
|
11
src/tools/chronometer/chronometer.service.ts
Normal file
11
src/tools/chronometer/chronometer.service.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export function formatMs(msTotal: number) {
|
||||
const ms = msTotal % 1000;
|
||||
const secs = ((msTotal - ms) / 1000) % 60;
|
||||
const mins = (((msTotal - ms) / 1000 - secs) / 60) % 60;
|
||||
const hrs = (((msTotal - ms) / 1000 - secs) / 60 - mins) / 60;
|
||||
const hrsString = hrs > 0 ? `${hrs.toString().padStart(2, '0')}:` : '';
|
||||
|
||||
return `${hrsString}${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms
|
||||
.toString()
|
||||
.padStart(3, '0')}`;
|
||||
}
|
53
src/tools/chronometer/chronometer.vue
Normal file
53
src/tools/chronometer/chronometer.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card>
|
||||
<div class="duration">{{ formatMs(counter) }}</div>
|
||||
</n-card>
|
||||
<br />
|
||||
<n-space justify="center">
|
||||
<n-button v-if="!isRunning" secondary type="primary" @click="resume">Start</n-button>
|
||||
<n-button v-else secondary type="warning" @click="pause">Stop</n-button>
|
||||
|
||||
<n-button secondary @click="counter = 0">Reset</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRafFn } from '@vueuse/core';
|
||||
import { ref } from 'vue';
|
||||
import { formatMs } from './chronometer.service';
|
||||
|
||||
const isRunning = ref(false);
|
||||
const counter = ref(0);
|
||||
|
||||
let previousRafDate = Date.now();
|
||||
const { pause: pauseRaf, resume: resumeRaf } = useRafFn(
|
||||
() => {
|
||||
const deltaMs = Date.now() - previousRafDate;
|
||||
previousRafDate = Date.now();
|
||||
counter.value += deltaMs;
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
function resume() {
|
||||
previousRafDate = Date.now();
|
||||
resumeRaf();
|
||||
isRunning.value = true;
|
||||
}
|
||||
|
||||
function pause() {
|
||||
pauseRaf();
|
||||
isRunning.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.duration {
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
font-family: monospace;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
11
src/tools/chronometer/index.ts
Normal file
11
src/tools/chronometer/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { TimerOutlined } from '@vicons/material';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Chronometer',
|
||||
path: '/chronometer',
|
||||
description: 'Monitor the duration of a thing. Basically a chronometer with simple chronometer features.',
|
||||
keywords: ['chronometer', 'time', 'lap', 'duration', 'measure', 'pause', 'resume', 'stopwatch'],
|
||||
component: () => import('./chronometer.vue'),
|
||||
icon: TimerOutlined,
|
||||
});
|
@@ -36,12 +36,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { colord, extend } from 'colord';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
|
||||
import cmykPlugin from 'colord/plugins/cmyk';
|
||||
import hwbPlugin from 'colord/plugins/hwb';
|
||||
import namesPlugin from 'colord/plugins/names';
|
||||
import lchPlugin from 'colord/plugins/lch';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
|
||||
extend([cmykPlugin, hwbPlugin, namesPlugin, lchPlugin]);
|
||||
|
||||
|
@@ -132,5 +132,10 @@ const formats: Format[] = [
|
||||
fromDate: (date) => date.toUTCString(),
|
||||
toDate,
|
||||
},
|
||||
{
|
||||
name: 'Mongo ObjectID',
|
||||
fromDate: (date) => Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000',
|
||||
toDate: (objectId) => new Date(parseInt(objectId.substring(0, 8), 16) * 1000),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
@@ -1,16 +1,17 @@
|
||||
<template>
|
||||
<n-card v-for="{ name, information } in sections" :key="name" :title="name">
|
||||
<n-grid cols="1 400:2" x-gap="12" y-gap="12">
|
||||
<n-gi v-for="{ label, value } in information" :key="label" class="information">
|
||||
<n-gi v-for="{ label, value: { value } } in information" :key="label" class="information">
|
||||
<n-card :bordered="false" embedded>
|
||||
<div class="label">
|
||||
{{ label }}
|
||||
</div>
|
||||
|
||||
<div class="value">
|
||||
<n-ellipsis>
|
||||
{{ value.value }}
|
||||
<n-ellipsis v-if="value">
|
||||
{{ value }}
|
||||
</n-ellipsis>
|
||||
<div v-else class="undefined-value">unknown</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
@@ -89,7 +90,10 @@ const sections = [
|
||||
.value {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.undefined-value {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
16
src/tools/eta-calculator/eta-calculator.service.ts
Normal file
16
src/tools/eta-calculator/eta-calculator.service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { formatDuration } from 'date-fns';
|
||||
|
||||
export function formatMsDuration(duration: number) {
|
||||
const ms = Math.floor(duration % 1000);
|
||||
const secs = Math.floor(((duration - ms) / 1000) % 60);
|
||||
const mins = Math.floor((((duration - ms) / 1000 - secs) / 60) % 60);
|
||||
const hrs = Math.floor((((duration - ms) / 1000 - secs) / 60 - mins) / 60);
|
||||
|
||||
return (
|
||||
formatDuration({
|
||||
hours: hrs,
|
||||
minutes: mins,
|
||||
seconds: secs,
|
||||
}) + (ms > 0 ? ` ${ms} ms` : '')
|
||||
);
|
||||
}
|
83
src/tools/eta-calculator/eta-calculator.vue
Normal file
83
src/tools/eta-calculator/eta-calculator.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-text depth="3" style="text-align: justify; width: 100%; display: inline-block">
|
||||
With a concrete example, if you wash 3 plates in 5 minutes and you have 500 plates to wash, it will take you 5
|
||||
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>
|
||||
<n-space item-style="flex:1 1 0">
|
||||
<n-form-item label="Amount of element to consume">
|
||||
<n-input-number v-model:value="unitCount" :min="1" />
|
||||
</n-form-item>
|
||||
<n-form-item label="The consumption started at">
|
||||
<n-date-picker v-model:value="startedAt" type="datetime" />
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
|
||||
<n-form-item label="Amount of unit consumed by time span" :show-feedback="false">
|
||||
<n-input-number v-model:value="unitPerTimeSpan" :min="1" />
|
||||
<span style="margin: 0 10px">in</span>
|
||||
<n-input-group>
|
||||
<n-input-number v-model:value="timeSpan" :min="1" />
|
||||
<n-select
|
||||
v-model:value="timeSpanUnitMultiplier"
|
||||
:options="[
|
||||
{ label: 'milliseconds', value: 1 },
|
||||
{ label: 'seconds', value: 1000 },
|
||||
{ label: 'minutes', value: 1000 * 60 },
|
||||
{ label: 'hours', value: 1000 * 60 * 60 },
|
||||
{ label: 'days', value: 1000 * 60 * 60 * 24 },
|
||||
]"
|
||||
></n-select>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-divider />
|
||||
<n-space vertical>
|
||||
<n-card>
|
||||
<n-statistic label="Total duration">{{ formatMsDuration(durationMs) }}</n-statistic>
|
||||
</n-card>
|
||||
<n-card>
|
||||
<n-statistic label="It will end ">{{ endAt }}</n-statistic>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</div>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Duplicate issue with sub directory
|
||||
// eslint-disable-next-line import/no-duplicates
|
||||
import { addMilliseconds, formatRelative } from 'date-fns';
|
||||
// eslint-disable-next-line import/no-duplicates
|
||||
import { enGB } from 'date-fns/locale';
|
||||
import { computed, ref } from 'vue';
|
||||
import { formatMsDuration } from './eta-calculator.service';
|
||||
|
||||
const unitCount = ref(3 * 62);
|
||||
const unitPerTimeSpan = ref(3);
|
||||
const timeSpan = ref(5);
|
||||
const timeSpanUnitMultiplier = ref(60000);
|
||||
const startedAt = ref(Date.now());
|
||||
|
||||
const durationMs = computed(() => {
|
||||
const timeSpanMs = timeSpan.value * timeSpanUnitMultiplier.value;
|
||||
|
||||
return unitCount.value / (unitPerTimeSpan.value / timeSpanMs);
|
||||
});
|
||||
const endAt = computed(() =>
|
||||
formatRelative(addMilliseconds(startedAt.value, durationMs.value), Date.now(), { locale: enGB }),
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-input-number,
|
||||
.n-date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
12
src/tools/eta-calculator/index.ts
Normal file
12
src/tools/eta-calculator/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Hourglass } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'ETA calculator',
|
||||
path: '/eta-calculator',
|
||||
description:
|
||||
'An ETA (Estimated Time of Arrival) calculator to know the approximate end time of a task, for example the moment of ending of a download.',
|
||||
keywords: ['eta', 'calculator', 'estimated', 'time', 'arrival', 'average'],
|
||||
component: () => import('./eta-calculator.vue'),
|
||||
icon: Hourglass,
|
||||
});
|
@@ -5,8 +5,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Memo from './git-memo.md';
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import Memo from './git-memo.md';
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
</script>
|
||||
|
15
src/tools/hash-text/hash-text.service.test.ts
Normal file
15
src/tools/hash-text/hash-text.service.test.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { convertHexToBin } from './hash-text.service';
|
||||
|
||||
describe('hash text', () => {
|
||||
describe('convertHexToBin', () => {
|
||||
it('convert hex to bin', () => {
|
||||
expect(convertHexToBin('')).toEqual('');
|
||||
expect(convertHexToBin('FF')).toEqual('11111111');
|
||||
expect(convertHexToBin('F'.repeat(200))).toEqual('1111'.repeat(200));
|
||||
expect(convertHexToBin('2123006AD00F694CE120')).toEqual(
|
||||
'00100001001000110000000001101010110100000000111101101001010011001110000100100000',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
7
src/tools/hash-text/hash-text.service.ts
Normal file
7
src/tools/hash-text/hash-text.service.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function convertHexToBin(hex: string) {
|
||||
return hex
|
||||
.trim()
|
||||
.split('')
|
||||
.map((byte) => parseInt(byte, 16).toString(2).padStart(4, '0'))
|
||||
.join('');
|
||||
}
|
@@ -1,10 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card>
|
||||
<n-input v-model:value="clearText" type="textarea" placeholder="Your string..." :autosize="{ minRows: 3 }" />
|
||||
<n-input v-model:value="clearText" type="textarea" placeholder="Your string to hash..." rows="3" />
|
||||
|
||||
<n-divider />
|
||||
|
||||
<n-form-item label="Digest encoding">
|
||||
<n-select
|
||||
v-model:value="encoding"
|
||||
:options="[
|
||||
{
|
||||
label: 'Binary (base 2)',
|
||||
value: 'Bin',
|
||||
},
|
||||
{
|
||||
label: 'Hexadecimal (base 16)',
|
||||
value: 'Hex',
|
||||
},
|
||||
{
|
||||
label: 'Base64 (base 64)',
|
||||
value: 'Base64',
|
||||
},
|
||||
{
|
||||
label: 'Base64url (base 64 with url safe chars)',
|
||||
value: 'Base64url',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<div v-for="algo in algoNames" :key="algo" style="margin: 5px 0">
|
||||
<n-input-group>
|
||||
<n-input-group-label style="flex: 0 0 120px"> {{ algo }} </n-input-group-label>
|
||||
@@ -16,9 +40,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
import { enc, lib, MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512 } from 'crypto-js';
|
||||
import { ref } from 'vue';
|
||||
import { MD5, SHA1, SHA256, SHA224, SHA512, SHA384, SHA3, RIPEMD160 } from 'crypto-js';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
import { convertHexToBin } from './hash-text.service';
|
||||
|
||||
const algos = {
|
||||
MD5,
|
||||
@@ -32,10 +57,18 @@ const algos = {
|
||||
} as const;
|
||||
|
||||
type AlgoNames = keyof typeof algos;
|
||||
type Encoding = keyof typeof enc | 'Bin';
|
||||
const algoNames = Object.keys(algos) as AlgoNames[];
|
||||
const encoding = ref<Encoding>('Hex');
|
||||
const clearText = ref('');
|
||||
|
||||
const clearText = ref(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lacus metus blandit dolor lacus natoque ad fusce aliquam velit.',
|
||||
);
|
||||
const hashText = (algo: AlgoNames, value: string) => algos[algo](value).toString();
|
||||
function formatWithEncoding(words: lib.WordArray, encoding: Encoding) {
|
||||
if (encoding === 'Bin') {
|
||||
return convertHexToBin(words.toString(enc.Hex));
|
||||
}
|
||||
|
||||
return words.toString(enc[encoding]);
|
||||
}
|
||||
|
||||
const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[algo](value), encoding.value);
|
||||
</script>
|
||||
|
98
src/tools/hmac-generator/hmac-generator.vue
Normal file
98
src/tools/hmac-generator/hmac-generator.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-form-item label="Plain text to compute the hash">
|
||||
<n-input v-model:value="plainText" type="textarea" placeholder="Enter the text to compute the hash..." />
|
||||
</n-form-item>
|
||||
<n-form-item label="Secret key">
|
||||
<n-input v-model:value="secret" placeholder="Enter the secret key..." />
|
||||
</n-form-item>
|
||||
<n-space item-style="flex:1 1 0">
|
||||
<n-form-item label="Hashing function">
|
||||
<n-select
|
||||
v-model:value="hashFunction"
|
||||
placeholder="Select an hashing function..."
|
||||
:options="Object.keys(algos).map((label) => ({ label, value: label }))"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Output encoding">
|
||||
<n-select
|
||||
v-model:value="encoding"
|
||||
placeholder="Select the result encoding..."
|
||||
:options="[
|
||||
{
|
||||
label: 'Binary (base 2)',
|
||||
value: 'Bin',
|
||||
},
|
||||
{
|
||||
label: 'Hexadecimal (base 16)',
|
||||
value: 'Hex',
|
||||
},
|
||||
{
|
||||
label: 'Base64 (base 64)',
|
||||
value: 'Base64',
|
||||
},
|
||||
{
|
||||
label: 'Base64-url (base 64 with url safe chars)',
|
||||
value: 'Base64url',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
<n-form-item label="HMAC of your text">
|
||||
<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>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import {
|
||||
enc,
|
||||
HmacMD5,
|
||||
HmacRIPEMD160,
|
||||
HmacSHA1,
|
||||
HmacSHA224,
|
||||
HmacSHA256,
|
||||
HmacSHA3,
|
||||
HmacSHA384,
|
||||
HmacSHA512,
|
||||
lib,
|
||||
} from 'crypto-js';
|
||||
import { computed, ref } from 'vue';
|
||||
import { convertHexToBin } from '../hash-text/hash-text.service';
|
||||
|
||||
const algos = {
|
||||
MD5: HmacMD5,
|
||||
RIPEMD160: HmacRIPEMD160,
|
||||
SHA1: HmacSHA1,
|
||||
SHA3: HmacSHA3,
|
||||
SHA224: HmacSHA224,
|
||||
SHA256: HmacSHA256,
|
||||
SHA384: HmacSHA384,
|
||||
SHA512: HmacSHA512,
|
||||
} as const;
|
||||
|
||||
type Encoding = keyof typeof enc | 'Bin';
|
||||
|
||||
function formatWithEncoding(words: lib.WordArray, encoding: Encoding) {
|
||||
if (encoding === 'Bin') {
|
||||
return convertHexToBin(words.toString(enc.Hex));
|
||||
}
|
||||
return words.toString(enc[encoding]);
|
||||
}
|
||||
|
||||
const plainText = ref('');
|
||||
const secret = ref('');
|
||||
const hashFunction = ref<keyof typeof algos>('SHA256');
|
||||
const encoding = ref<Encoding>('Hex');
|
||||
const hmac = computed(() =>
|
||||
formatWithEncoding(algos[hashFunction.value](plainText.value, secret.value), encoding.value),
|
||||
);
|
||||
const { copy } = useCopy({ source: hmac });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
12
src/tools/hmac-generator/index.ts
Normal file
12
src/tools/hmac-generator/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ShortTextRound } from '@vicons/material';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Hmac generator',
|
||||
path: '/hmac-generator',
|
||||
description:
|
||||
'Computes a hash-based message authentication code (HMAC) using a secret key and your favorite hashing function.',
|
||||
keywords: ['hmac', 'generator', 'MD5', 'SHA1', 'SHA256', 'SHA224', 'SHA512', 'SHA384', 'SHA3', 'RIPEMD160'],
|
||||
component: () => import('./hmac-generator.vue'),
|
||||
icon: ShortTextRound,
|
||||
});
|
@@ -1,36 +1,47 @@
|
||||
import { LockOpen } from '@vicons/tabler';
|
||||
import type { ToolCategory } from './tool';
|
||||
|
||||
import { tool as mathEvaluator } from './math-evaluator';
|
||||
import { tool as jsonViewer } from './json-viewer';
|
||||
import { tool as htmlEntities } from './html-entities';
|
||||
import { tool as urlParser } from './url-parser';
|
||||
import { tool as deviceInformation } from './device-information';
|
||||
import { tool as chmodCalculator } from './chmod-calculator';
|
||||
import { tool as mimeTypes } from './mime-types';
|
||||
import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
|
||||
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 bcrypt } from './bcrypt';
|
||||
import { tool as bip39 } from './bip39-generator';
|
||||
import { tool as caseConverter } from './case-converter';
|
||||
import { tool as chronometer } from './chronometer';
|
||||
import { tool as colorConverter } from './color-converter';
|
||||
import { tool as qrCodeGenerator } from './qr-code-generator';
|
||||
import { tool as base64Converter } from './base64-converter';
|
||||
import { tool as crontabGenerator } from './crontab-generator';
|
||||
import { tool as dateTimeConverter } from './date-time-converter';
|
||||
import { tool as deviceInformation } from './device-information';
|
||||
import { tool as cypher } from './encryption';
|
||||
import { tool as etaCalculator } from './eta-calculator';
|
||||
import { tool as gitMemo } from './git-memo';
|
||||
import { tool as hashText } from './hash-text';
|
||||
import { tool as hmacGenerator } from './hmac-generator';
|
||||
import { tool as htmlEntities } from './html-entities';
|
||||
import { tool as baseConverter } from './integer-base-converter';
|
||||
import { tool as jsonViewer } from './json-viewer';
|
||||
import { tool as loremIpsumGenerator } from './lorem-ipsum-generator';
|
||||
import { tool as mathEvaluator } from './math-evaluator';
|
||||
import { tool as metaTagGenerator } from './meta-tag-generator';
|
||||
import { tool as qrCodeGenerator } from './qr-code-generator';
|
||||
import { tool as randomPortGenerator } from './random-port-generator';
|
||||
import { tool as romanNumeralConverter } from './roman-numeral-converter';
|
||||
import { tool as sqlPrettify } from './sql-prettify';
|
||||
import { tool as svgPlaceholderGenerator } from './svg-placeholder-generator';
|
||||
import { tool as textStatistics } from './text-statistics';
|
||||
import { tool as tokenGenerator } from './token-generator';
|
||||
import { tool as hashText } from './hash-text';
|
||||
import { tool as uuidGenerator } from './uuid-generator';
|
||||
import { tool as romanNumeralConverter } from './roman-numeral-converter';
|
||||
import { tool as cypher } from './encryption';
|
||||
import { tool as bip39 } from './bip39-generator';
|
||||
import { tool as dateTimeConverter } from './date-time-converter';
|
||||
import { tool as gitMemo } from './git-memo';
|
||||
import { tool as baseConverter } from './integer-base-converter';
|
||||
import { tool as urlEncoder } from './url-encoder';
|
||||
import { tool as randomPortGenerator } from './random-port-generator';
|
||||
import { tool as loremIpsumGenerator } from './lorem-ipsum-generator';
|
||||
import { tool as urlParser } from './url-parser';
|
||||
import { tool as uuidGenerator } from './uuid-generator';
|
||||
|
||||
export const toolsByCategory: ToolCategory[] = [
|
||||
{
|
||||
name: 'Crypto',
|
||||
icon: LockOpen,
|
||||
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39],
|
||||
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator],
|
||||
},
|
||||
{
|
||||
name: 'Converter',
|
||||
@@ -39,7 +50,8 @@ export const toolsByCategory: ToolCategory[] = [
|
||||
dateTimeConverter,
|
||||
baseConverter,
|
||||
romanNumeralConverter,
|
||||
base64Converter,
|
||||
base64StringConverter,
|
||||
base64FileConverter,
|
||||
colorConverter,
|
||||
caseConverter,
|
||||
],
|
||||
@@ -47,17 +59,36 @@ export const toolsByCategory: ToolCategory[] = [
|
||||
{
|
||||
name: 'Web',
|
||||
icon: LockOpen,
|
||||
components: [urlEncoder, htmlEntities, qrCodeGenerator, urlParser, deviceInformation],
|
||||
components: [
|
||||
urlEncoder,
|
||||
htmlEntities,
|
||||
urlParser,
|
||||
deviceInformation,
|
||||
basicAuthGenerator,
|
||||
metaTagGenerator,
|
||||
otpCodeGeneratorAndValidator,
|
||||
mimeTypes,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Images',
|
||||
icon: LockOpen,
|
||||
components: [qrCodeGenerator, svgPlaceholderGenerator],
|
||||
},
|
||||
{
|
||||
name: 'Development',
|
||||
icon: LockOpen,
|
||||
components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer],
|
||||
components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify, chmodCalculator],
|
||||
},
|
||||
{
|
||||
name: 'Math',
|
||||
icon: LockOpen,
|
||||
components: [mathEvaluator],
|
||||
components: [mathEvaluator, etaCalculator],
|
||||
},
|
||||
{
|
||||
name: 'Measurement',
|
||||
icon: LockOpen,
|
||||
components: [chronometer],
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
|
@@ -67,9 +67,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
import { convertBase } from './integer-base-converter.model';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
|
||||
const styleStore = useStyleStore();
|
||||
|
||||
|
@@ -2,10 +2,11 @@ import { Braces } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'JSON viewer',
|
||||
path: '/json-viewer',
|
||||
description: 'Prettify JSON string to a human friendly readable format.',
|
||||
name: 'JSON prettify and format',
|
||||
path: '/json-prettify',
|
||||
description: 'Prettify your JSON string to a human friendly readable format.',
|
||||
keywords: ['json', 'viewer', 'prettify', 'format'],
|
||||
component: () => import('./json-viewer.vue'),
|
||||
icon: Braces,
|
||||
redirectFrom: ['/json-viewer'],
|
||||
});
|
||||
|
@@ -1,66 +1,56 @@
|
||||
<template>
|
||||
<n-card>
|
||||
<n-form-item
|
||||
label="Your raw json:"
|
||||
:feedback="rawJsonValidation.message"
|
||||
:validation-status="rawJsonValidation.status"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="rawJson"
|
||||
class="json-input"
|
||||
type="textarea"
|
||||
placeholder="Paste your raw json here..."
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="rawJson = ''">Clear</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card v-if="cleanJson.length > 0">
|
||||
<n-scrollbar :x-scrollable="true">
|
||||
<n-config-provider :hljs="hljs">
|
||||
<n-code :code="cleanJson" language="json" />
|
||||
</n-config-provider>
|
||||
</n-scrollbar>
|
||||
</n-card>
|
||||
<n-form-item
|
||||
label="Your raw json"
|
||||
:feedback="rawJsonValidation.message"
|
||||
:validation-status="rawJsonValidation.status"
|
||||
>
|
||||
<n-input
|
||||
ref="inputElement"
|
||||
v-model:value="rawJson"
|
||||
placeholder="Paste your raw json here..."
|
||||
type="textarea"
|
||||
rows="20"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Prettify version of your json">
|
||||
<textarea-copyable :value="cleanJson" language="json" :follow-height-of="inputElement" />
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import json from 'highlight.js/lib/languages/json';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import JSON5 from 'json5';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
hljs.registerLanguage('json', json);
|
||||
const inputElement = ref<HTMLElement>();
|
||||
|
||||
const rawJson = ref('');
|
||||
const cleanJson = computed(() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(rawJson.value), null, 3);
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
const rawJson = ref('{"hello": "world"}');
|
||||
const cleanJson = computed(() => withDefaultOnError(() => JSON.stringify(JSON5.parse(rawJson.value), null, 3), ''));
|
||||
|
||||
const rawJsonValidation = useValidation({
|
||||
source: rawJson,
|
||||
rules: [
|
||||
{
|
||||
validator: (v) => v === '' || JSON.parse(v),
|
||||
message: 'Invalid json string',
|
||||
validator: (v) => v === '' || JSON5.parse(v),
|
||||
message: 'Provided JSON is not valid.',
|
||||
},
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.json-input ::v-deep(.n-input-wrapper) {
|
||||
resize: both !important;
|
||||
.result-card {
|
||||
position: relative;
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -30,8 +30,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { ref, computed } from 'vue';
|
||||
import { generateLoremIpsum } from './lorem-ipsum-generator.service';
|
||||
import { randIntFromInterval } from '@/utils/random';
|
||||
import { generateLoremIpsum } from './lorem-ipsum-generator.service';
|
||||
|
||||
const paragraphs = ref(1);
|
||||
const sentences = ref([3, 8]);
|
||||
|
@@ -21,18 +21,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { evaluate } from 'mathjs';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const expression = ref('');
|
||||
|
||||
const result = computed(() => {
|
||||
try {
|
||||
return evaluate(expression.value) ?? '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
const result = computed(() => withDefaultOnError(() => evaluate(expression.value) ?? '', ''));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
27
src/tools/meta-tag-generator/OGSchemaType.type.ts
Normal file
27
src/tools/meta-tag-generator/OGSchemaType.type.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { SelectGroupOption, SelectOption } from 'naive-ui';
|
||||
|
||||
export type { OGSchemaType, OGSchemaTypeElementInput, OGSchemaTypeElementSelect, OGSchemaTypeElementInputMultiple };
|
||||
|
||||
interface OGSchemaTypeElementBase {
|
||||
key: string;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
interface OGSchemaTypeElementInput extends OGSchemaTypeElementBase {
|
||||
type: 'input';
|
||||
}
|
||||
|
||||
interface OGSchemaTypeElementInputMultiple extends OGSchemaTypeElementBase {
|
||||
type: 'input-multiple';
|
||||
}
|
||||
|
||||
interface OGSchemaTypeElementSelect extends OGSchemaTypeElementBase {
|
||||
type: 'select';
|
||||
options: Array<SelectOption | SelectGroupOption>;
|
||||
}
|
||||
|
||||
interface OGSchemaType {
|
||||
name: string;
|
||||
elements: (OGSchemaTypeElementSelect | OGSchemaTypeElementInput | OGSchemaTypeElementInputMultiple)[];
|
||||
}
|
25
src/tools/meta-tag-generator/index.ts
Normal file
25
src/tools/meta-tag-generator/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Tags } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Open graph meta generator',
|
||||
path: '/og-meta-generator',
|
||||
description: 'Generate open-graph and socials html meta tags for your website.',
|
||||
keywords: [
|
||||
'meta',
|
||||
'tag',
|
||||
'generator',
|
||||
'social',
|
||||
'title',
|
||||
'description',
|
||||
'image',
|
||||
'share',
|
||||
'online',
|
||||
'website',
|
||||
'open',
|
||||
'graph',
|
||||
'og',
|
||||
],
|
||||
component: () => import('./meta-tag-generator.vue'),
|
||||
icon: Tags,
|
||||
});
|
94
src/tools/meta-tag-generator/meta-tag-generator.vue
Normal file
94
src/tools/meta-tag-generator/meta-tag-generator.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="{ name, elements } of sections" :key="name" style="margin-bottom: 15px">
|
||||
<n-form-item :label="name" :show-feedback="false"> </n-form-item>
|
||||
|
||||
<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" />
|
||||
<n-dynamic-input
|
||||
v-else-if="type === 'input-multiple'"
|
||||
v-model:value="metadata[key]"
|
||||
:min="1"
|
||||
:placeholder="placeholder"
|
||||
:default-value="['']"
|
||||
:show-sort-button="true"
|
||||
/>
|
||||
|
||||
<n-select
|
||||
v-else-if="type === 'select'"
|
||||
v-model:value="metadata[key]"
|
||||
:placeholder="placeholder"
|
||||
:options="(element as OGSchemaTypeElementSelect).options"
|
||||
/>
|
||||
</n-input-group>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<n-form-item label="Your meta tags">
|
||||
<textarea-copyable :value="metaTags" language="html" />
|
||||
</n-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { generateMeta } from '@it-tools/oggen';
|
||||
import _ from 'lodash';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { image, ogSchemas, twitter, website } from './og-schemas';
|
||||
import type { OGSchemaType, OGSchemaTypeElementSelect } from './OGSchemaType.type';
|
||||
|
||||
// Since type guards do not work in template
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const metadata = ref<{ type: string; [k: string]: any }>({
|
||||
type: 'website',
|
||||
'twitter:card': 'summary_large_image',
|
||||
});
|
||||
|
||||
watch(
|
||||
() => ref(metadata.value.type),
|
||||
(_ignored, prevSection) => {
|
||||
const section = ogSchemas[prevSection.value];
|
||||
|
||||
if (!section) return;
|
||||
|
||||
section.elements.forEach(({ key }) => {
|
||||
metadata.value[key] = '';
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const sections = computed(() => {
|
||||
const secs: OGSchemaType[] = [website, image, twitter];
|
||||
const additionalSchema = ogSchemas[metadata.value.type];
|
||||
|
||||
if (additionalSchema) secs.push(additionalSchema);
|
||||
|
||||
return secs;
|
||||
});
|
||||
|
||||
const metaTags = computed(() => {
|
||||
const twitterMeta = _.chain(metadata.value)
|
||||
.pickBy((_value, k) => k.startsWith('twitter:'))
|
||||
.mapKeys((_value, k) => k.replace(/^twitter:/, ''))
|
||||
.value();
|
||||
|
||||
const otherMeta = _.pickBy(metadata.value, (_value, k) => !k.startsWith('twitter:'));
|
||||
|
||||
return generateMeta({ ...otherMeta, twitter: twitterMeta }, { generateTwitterCompatibleMeta: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-input-group {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
::v-deep(.n-form-item-blank) {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
::v-deep(.n-dynamic-input-item) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
</style>
|
33
src/tools/meta-tag-generator/og-schemas/article.ts
Normal file
33
src/tools/meta-tag-generator/og-schemas/article.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
export const article: OGSchemaType = {
|
||||
name: 'Article',
|
||||
elements: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Publishing date',
|
||||
key: 'article:published_time',
|
||||
placeholder: 'When the article was first published...',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Modification date',
|
||||
key: 'article:modified_time',
|
||||
placeholder: 'When the article was last changed...',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Expiration date',
|
||||
key: 'article:expiration_time',
|
||||
placeholder: 'When the article is out of date after...',
|
||||
},
|
||||
{ type: 'input', label: 'Author', key: 'article:author', placeholder: 'Writers of the article...' },
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Section',
|
||||
key: 'article:section',
|
||||
placeholder: 'A high-level section name. E.g. Technology..',
|
||||
},
|
||||
{ type: 'input', label: 'Tag', key: 'article:tag', placeholder: 'Tag words associated with this article...' },
|
||||
],
|
||||
};
|
16
src/tools/meta-tag-generator/og-schemas/book.ts
Normal file
16
src/tools/meta-tag-generator/og-schemas/book.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
export const book: OGSchemaType = {
|
||||
name: 'Book',
|
||||
elements: [
|
||||
{ type: 'input', label: 'Author', key: 'book:author', placeholder: 'Who wrote this book...' },
|
||||
{ type: 'input', label: 'ISBN', key: 'book:isbn', placeholder: 'The International Standard Book Number...' },
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Release date',
|
||||
key: 'book:release_date',
|
||||
placeholder: 'The date the book was released...',
|
||||
},
|
||||
{ type: 'input', label: 'Tag', key: 'book:tag', placeholder: 'Tag words associated with this book...' },
|
||||
],
|
||||
};
|
31
src/tools/meta-tag-generator/og-schemas/image.ts
Normal file
31
src/tools/meta-tag-generator/og-schemas/image.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
export const image: OGSchemaType = {
|
||||
name: 'Image',
|
||||
elements: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Image url',
|
||||
placeholder: 'The url of your website social image...',
|
||||
key: 'image',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Image alt',
|
||||
placeholder: 'The alternative text of your website social image...',
|
||||
key: 'image:alt',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Width',
|
||||
placeholder: 'Width in px of your website social image...',
|
||||
key: 'image:width',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Height',
|
||||
placeholder: 'Height in px of your website social image...',
|
||||
key: 'image:height',
|
||||
},
|
||||
],
|
||||
};
|
31
src/tools/meta-tag-generator/og-schemas/index.ts
Normal file
31
src/tools/meta-tag-generator/og-schemas/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
import { article } from './article';
|
||||
import { book } from './book';
|
||||
import { musicAlbum } from './musicAlbum';
|
||||
import { musicPlaylist } from './musicPlaylist';
|
||||
import { musicRadioStation } from './musicRadioStation';
|
||||
import { musicSong } from './musicSong';
|
||||
import { profile } from './profile';
|
||||
import { videoEpisode } from './videoEpisode';
|
||||
import { videoMovie } from './videoMovie';
|
||||
import { videoOther } from './videoOther';
|
||||
import { videoTVShow } from './videoTVShow';
|
||||
|
||||
export * from './image';
|
||||
export * from './twitter';
|
||||
export * from './website';
|
||||
|
||||
export const ogSchemas: Record<string, OGSchemaType> = {
|
||||
'music.song': musicSong,
|
||||
'music.album': musicAlbum,
|
||||
'music.playlist': musicPlaylist,
|
||||
'music.radio_station': musicRadioStation,
|
||||
'video.movie': videoMovie,
|
||||
'video.episode': videoEpisode,
|
||||
'video.tv_show': videoTVShow,
|
||||
'video.other': videoOther,
|
||||
profile,
|
||||
article,
|
||||
book,
|
||||
};
|
27
src/tools/meta-tag-generator/og-schemas/musicAlbum.ts
Normal file
27
src/tools/meta-tag-generator/og-schemas/musicAlbum.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
export const musicAlbum: OGSchemaType = {
|
||||
name: 'Album details',
|
||||
elements: [
|
||||
{ type: 'input', label: 'Song', key: 'music:song', placeholder: 'The song on this album...' },
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Disc',
|
||||
key: 'music:song:disc',
|
||||
placeholder: 'The same as music:album:disc but in reverse...',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Track',
|
||||
key: 'music:song:track',
|
||||
placeholder: 'The same as music:album:track but in reverse...',
|
||||
},
|
||||
{ type: 'input', label: 'Musician', key: 'music:musician', placeholder: 'The musician that made this song...' },
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Release date',
|
||||
key: 'music:release_date',
|
||||
placeholder: 'The date the album was released...',
|
||||
},
|
||||
],
|
||||
};
|
21
src/tools/meta-tag-generator/og-schemas/musicPlaylist.ts
Normal file
21
src/tools/meta-tag-generator/og-schemas/musicPlaylist.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
export const musicPlaylist: OGSchemaType = {
|
||||
name: 'Playlist details',
|
||||
elements: [
|
||||
{ type: 'input', label: 'Song', key: 'music:song', placeholder: 'The song on this album...' },
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Disc',
|
||||
key: 'music:song:disc',
|
||||
placeholder: 'The same as music:album:disc but in reverse...',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Track',
|
||||
key: 'music:song:track',
|
||||
placeholder: 'The same as music:album:track but in reverse...',
|
||||
},
|
||||
{ type: 'input', label: 'Creator', key: 'music:creator', placeholder: 'The creator of this playlist...' },
|
||||
],
|
||||
};
|
@@ -0,0 +1,8 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
export const musicRadioStation: OGSchemaType = {
|
||||
name: 'Radio station details',
|
||||
elements: [
|
||||
{ type: 'input', label: 'Creator', key: 'music:creator', placeholder: 'The creator of this radio station...' },
|
||||
],
|
||||
};
|
22
src/tools/meta-tag-generator/og-schemas/musicSong.ts
Normal file
22
src/tools/meta-tag-generator/og-schemas/musicSong.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
export const musicSong: OGSchemaType = {
|
||||
name: 'Song details',
|
||||
elements: [
|
||||
{ type: 'input', label: 'Duration', placeholder: 'The duration of the song...', key: 'music:duration' },
|
||||
{ type: 'input', label: 'Album', placeholder: 'The album this song is from...', key: 'music:album' },
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Disc',
|
||||
placeholder: 'Which disc of the album this song is on...',
|
||||
key: 'music:album:disk',
|
||||
},
|
||||
{ type: 'input', label: 'Track', placeholder: ' Which track this song is...', key: 'music:album:track' },
|
||||
{
|
||||
type: 'input-multiple',
|
||||
label: 'Musician',
|
||||
placeholder: 'The musician that made this song...',
|
||||
key: 'music:musician',
|
||||
},
|
||||
],
|
||||
};
|
21
src/tools/meta-tag-generator/og-schemas/profile.ts
Normal file
21
src/tools/meta-tag-generator/og-schemas/profile.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
export const profile: OGSchemaType = {
|
||||
name: 'Profile',
|
||||
elements: [
|
||||
{
|
||||
type: 'input',
|
||||
label: 'First name',
|
||||
placeholder: 'Enter the first name of the person...',
|
||||
key: 'profile:first_name',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Last name',
|
||||
placeholder: 'Enter the last name of the person...',
|
||||
key: 'profile:last_name',
|
||||
},
|
||||
{ type: 'input', label: 'Username', placeholder: 'Enter the username of the person...', key: 'profile:username' },
|
||||
{ type: 'input', label: 'Gender', placeholder: 'Enter the gender of the person...', key: 'profile:gender' },
|
||||
],
|
||||
};
|
31
src/tools/meta-tag-generator/og-schemas/twitter.ts
Normal file
31
src/tools/meta-tag-generator/og-schemas/twitter.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
export const twitter: OGSchemaType = {
|
||||
name: 'Twitter',
|
||||
elements: [
|
||||
{
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Summary', value: 'summary' },
|
||||
{ label: 'Summary with large image', value: 'summary_large_image' },
|
||||
{ label: 'Application', value: 'app' },
|
||||
{ label: 'Player', value: 'player' },
|
||||
],
|
||||
label: 'Card type',
|
||||
placeholder: 'The twitter card type...',
|
||||
key: 'twitter:card',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Site account',
|
||||
placeholder: 'The name of the twitter account of the site (ex: @ittoolsdottech)...',
|
||||
key: 'twitter:site',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Creator acc.',
|
||||
placeholder: 'The name of the twitter account of the creator (ex: @cthmsst)...',
|
||||
key: 'twitter:creator',
|
||||
},
|
||||
],
|
||||
};
|
10
src/tools/meta-tag-generator/og-schemas/videoEpisode.ts
Normal file
10
src/tools/meta-tag-generator/og-schemas/videoEpisode.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
import { videoMovie } from './videoMovie';
|
||||
|
||||
export const videoEpisode: OGSchemaType = {
|
||||
name: 'Video episode details',
|
||||
elements: [
|
||||
...videoMovie.elements,
|
||||
{ type: 'input', label: 'Series', key: 'video:series', placeholder: 'Which series this episode belongs to...' },
|
||||
],
|
||||
};
|
29
src/tools/meta-tag-generator/og-schemas/videoMovie.ts
Normal file
29
src/tools/meta-tag-generator/og-schemas/videoMovie.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
export const videoMovie: OGSchemaType = {
|
||||
name: 'Movie details',
|
||||
elements: [
|
||||
{
|
||||
type: 'input-multiple',
|
||||
label: 'Actor',
|
||||
key: 'video:actor',
|
||||
placeholder: 'Name of the actress/actor...',
|
||||
},
|
||||
// { type: 'input', label: 'Actor role', key: 'video:actor:role', placeholder: 'The role they played...' },
|
||||
{
|
||||
type: 'input-multiple',
|
||||
label: 'Director',
|
||||
key: 'video:director',
|
||||
placeholder: 'Name of the director...',
|
||||
},
|
||||
{ type: 'input-multiple', label: 'Writer', key: 'video:writer', placeholder: 'Writers of the movie...' },
|
||||
{ type: 'input', label: 'Duration', key: 'video:duration', placeholder: "The movie's length in seconds..." },
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Release date',
|
||||
key: 'video:release_date',
|
||||
placeholder: 'The date the movie was released...',
|
||||
},
|
||||
{ type: 'input', label: 'Tag', key: 'video:tag', placeholder: 'Tag words associated with this movie...' },
|
||||
],
|
||||
};
|
7
src/tools/meta-tag-generator/og-schemas/videoOther.ts
Normal file
7
src/tools/meta-tag-generator/og-schemas/videoOther.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
import { videoMovie } from './videoMovie';
|
||||
|
||||
export const videoOther: OGSchemaType = {
|
||||
name: 'Other video details',
|
||||
elements: [...videoMovie.elements],
|
||||
};
|
7
src/tools/meta-tag-generator/og-schemas/videoTVShow.ts
Normal file
7
src/tools/meta-tag-generator/og-schemas/videoTVShow.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
import { videoMovie } from './videoMovie';
|
||||
|
||||
export const videoTVShow: OGSchemaType = {
|
||||
name: 'TV show details',
|
||||
elements: [...videoMovie.elements],
|
||||
};
|
56
src/tools/meta-tag-generator/og-schemas/website.ts
Normal file
56
src/tools/meta-tag-generator/og-schemas/website.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { OGSchemaType } from '../OGSchemaType.type';
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'Website', value: 'website' },
|
||||
{ label: 'Article', value: 'article' },
|
||||
{ label: 'Book', value: 'book' },
|
||||
{ label: 'Profile', value: 'profile' },
|
||||
{
|
||||
type: 'group',
|
||||
label: 'Music',
|
||||
key: 'Music',
|
||||
children: [
|
||||
{ label: 'Song', value: 'music.song' },
|
||||
{ label: 'Music album', value: 'music.album' },
|
||||
{ label: 'Playlist', value: 'music.playlist' },
|
||||
{ label: 'Radio station', value: 'music.radio_station' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
label: 'Video',
|
||||
key: 'Video',
|
||||
children: [
|
||||
{ label: 'Movie', value: 'video.movie' },
|
||||
{ label: 'Episode', value: 'video.episode' },
|
||||
{ label: 'TV show', value: 'video.tv_show' },
|
||||
{ label: 'Other video', value: 'video.other' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const website: OGSchemaType = {
|
||||
name: 'General information',
|
||||
elements: [
|
||||
{
|
||||
type: 'select',
|
||||
label: 'Page type',
|
||||
placeholder: 'Select the type of your website...',
|
||||
key: 'type',
|
||||
options: typeOptions,
|
||||
},
|
||||
{ type: 'input', label: 'Title', placeholder: 'Enter the title of your website...', key: 'title' },
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Description',
|
||||
placeholder: 'Enter the description of your website...',
|
||||
key: 'description',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Page URL',
|
||||
placeholder: 'Enter the url of your website...',
|
||||
key: 'url',
|
||||
},
|
||||
],
|
||||
};
|
11
src/tools/mime-types/index.ts
Normal file
11
src/tools/mime-types/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { World } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Mime types',
|
||||
path: '/mime-types',
|
||||
description: 'Convert mime types to extensions and vice-versa.',
|
||||
keywords: ['mime', 'types', 'extension', 'content', 'type'],
|
||||
component: () => import('./mime-types.vue'),
|
||||
icon: World,
|
||||
});
|
99
src/tools/mime-types/mime-types.vue
Normal file
99
src/tools/mime-types/mime-types.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<n-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>
|
||||
<n-select
|
||||
v-model:value="selectedMimeType"
|
||||
filterable
|
||||
:options="mimeToExtensionsOptions"
|
||||
size="large"
|
||||
placeholder="Select your mimetype here... (ex: application/pdf)"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<div v-if="extensionsFound.length > 0">
|
||||
Extensions of files with the <n-tag round :bordered="false">{{ selectedMimeType }}</n-tag> mime-type:
|
||||
<div style="margin-top: 10px">
|
||||
<n-tag
|
||||
v-for="extension of extensionsFound"
|
||||
:key="extension"
|
||||
round
|
||||
:bordered="false"
|
||||
type="primary"
|
||||
style="margin-right: 10px"
|
||||
>
|
||||
.{{ extension }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<n-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>
|
||||
<n-select
|
||||
v-model:value="selectedExtension"
|
||||
filterable
|
||||
:options="extensionToMimeTypeOptions"
|
||||
size="large"
|
||||
placeholder="Select your mimetype here... (ex: application/pdf)"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<div v-if="selectedExtension">
|
||||
Mime type associated to the extension <n-tag round :bordered="false">{{ selectedExtension }}</n-tag> file
|
||||
extension:
|
||||
<div style="margin-top: 10px">
|
||||
<n-tag round :bordered="false" type="primary" style="margin-right: 10px">
|
||||
{{ mimeTypeFound }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<div>
|
||||
<n-table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mime types</th>
|
||||
<th>Extensions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="{ mimeType, extensions } of mimeInfos" :key="mimeType">
|
||||
<td>{{ mimeType }}</td>
|
||||
<td>
|
||||
<n-tag v-for="extension of extensions" :key="extension" round :bordered="false" style="margin-right: 10px">
|
||||
.{{ extension }}
|
||||
</n-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { types as extensionToMimeType, extensions as mimeTypeToExtension } from 'mime-types';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const mimeInfos = Object.entries(mimeTypeToExtension).map(([mimeType, extensions]) => ({ mimeType, extensions }));
|
||||
|
||||
const mimeToExtensionsOptions = Object.keys(mimeTypeToExtension).map((label) => ({ label, value: label }));
|
||||
const selectedMimeType = ref(undefined);
|
||||
|
||||
const extensionsFound = computed(() => (selectedMimeType.value ? mimeTypeToExtension[selectedMimeType.value] : []));
|
||||
|
||||
const extensionToMimeTypeOptions = Object.keys(extensionToMimeType).map((label) => {
|
||||
const extension = `.${label}`;
|
||||
|
||||
return { label: extension, value: label };
|
||||
});
|
||||
const selectedExtension = ref(undefined);
|
||||
|
||||
const mimeTypeFound = computed(() => (selectedExtension.value ? extensionToMimeType[selectedExtension.value] : []));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
27
src/tools/otp-code-generator-and-validator/index.ts
Normal file
27
src/tools/otp-code-generator-and-validator/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { DeviceMobile } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'OTP code generator',
|
||||
path: '/otp-generator',
|
||||
description: 'Generate and validate time-based OTP (one time password) for multi-factor authentication.',
|
||||
keywords: [
|
||||
'otp',
|
||||
'code',
|
||||
'generator',
|
||||
'validator',
|
||||
'one',
|
||||
'time',
|
||||
'password',
|
||||
'authentication',
|
||||
'MFA',
|
||||
'mobile',
|
||||
'device',
|
||||
'security',
|
||||
'TOTP',
|
||||
'Time',
|
||||
'HMAC',
|
||||
],
|
||||
component: () => import('./otp-code-generator-and-validator.vue'),
|
||||
icon: DeviceMobile,
|
||||
});
|
@@ -0,0 +1,140 @@
|
||||
<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>
|
||||
|
||||
<div>
|
||||
<token-display :tokens="tokens" style="margin-top: 2px" />
|
||||
|
||||
<n-progress :percentage="(100 * interval) / 30" :color="theme.primaryColor" :show-indicator="false" />
|
||||
<div style="text-align: center">Next in {{ String(Math.floor(30 - interval)).padStart(2, '0') }}s</div>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Refresh } from '@vicons/tabler';
|
||||
import { useTimestamp, whenever } 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 { generateTOTP, buildKeyUri, generateSecret, base32toHex, getCounterFromTime } from './otp.service';
|
||||
import { useQRCode } from '../qr-code-generator/useQRCode';
|
||||
import TokenDisplay from './token-display.vue';
|
||||
|
||||
const now = useTimestamp();
|
||||
const interval = computed(() => (now.value / 1000) % 30);
|
||||
const theme = useThemeVars();
|
||||
const styleStore = useStyleStore();
|
||||
const secret = ref(generateSecret());
|
||||
const tokens = ref(buildTokens());
|
||||
const keyUri = computed(() => buildKeyUri({ secret: secret.value }));
|
||||
|
||||
const { qrcode } = useQRCode({
|
||||
text: keyUri,
|
||||
color: { background: '#00000000', foreground: computed(() => (styleStore.isDarkTheme ? '#ffffff' : '#000000')) },
|
||||
options: { width: 210 },
|
||||
});
|
||||
|
||||
const { attrs: secretValidationAttrs } = useValidation({
|
||||
source: secret,
|
||||
rules: [
|
||||
{
|
||||
message: 'Secret should be a base32 string',
|
||||
validator: (value) => value.match(/^[A-Z234567]+$/),
|
||||
},
|
||||
{
|
||||
message: 'Please set a secret',
|
||||
validator: (value) => value !== '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// watch + whenever to prevent token to be refresh every raf
|
||||
watch([secret], refreshToken);
|
||||
whenever(() => Math.floor(interval.value) === 0, refreshToken);
|
||||
|
||||
function refreshSecret() {
|
||||
secret.value = generateSecret();
|
||||
}
|
||||
|
||||
function refreshToken() {
|
||||
tokens.value = buildTokens();
|
||||
}
|
||||
|
||||
function buildTokens() {
|
||||
return {
|
||||
previous: generateTOTP({ key: secret.value, now: now.value - 30000 }),
|
||||
current: generateTOTP({ key: secret.value, now: now.value }),
|
||||
next: generateTOTP({ key: secret.value, now: now.value + 30000 }),
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-progress {
|
||||
margin-top: 10px;
|
||||
::v-deep(.n-progress-graph-line-fill) {
|
||||
transition-duration: 0.05s !important;
|
||||
}
|
||||
}
|
||||
|
||||
.token {
|
||||
text-align: center;
|
||||
&.token-current {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
124
src/tools/otp-code-generator-and-validator/otp.service.test.ts
Normal file
124
src/tools/otp-code-generator-and-validator/otp.service.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
generateHOTP,
|
||||
hexToBytes,
|
||||
verifyHOTP,
|
||||
generateTOTP,
|
||||
verifyTOTP,
|
||||
buildKeyUri,
|
||||
base32toHex,
|
||||
} from './otp.service';
|
||||
|
||||
describe('otp functions', () => {
|
||||
describe('hexToBytes', () => {
|
||||
it('convert an hexstring to a byte array', () => {
|
||||
expect(hexToBytes('1')).to.eql([1]);
|
||||
expect(hexToBytes('ffffff')).to.eql([255, 255, 255]);
|
||||
expect(hexToBytes('000000000')).to.eql([0, 0, 0, 0, 0]);
|
||||
expect(hexToBytes('a3218bcef89')).to.eql([163, 33, 139, 206, 248, 9]);
|
||||
expect(hexToBytes('063679ca')).toEqual([6, 54, 121, 202]);
|
||||
expect(hexToBytes('0102030405060708090a0b0c0d0e0f')).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
|
||||
});
|
||||
});
|
||||
describe('base32tohex', () => {
|
||||
it('convert a base32 to hex string', () => {
|
||||
expect(base32toHex('ABCDEF')).to.eql('00443205');
|
||||
expect(base32toHex('7777')).to.eql('ffff0f');
|
||||
expect(base32toHex('JBSWY3DPEHPK3PXP')).to.eql('48656c6c6f21deadbeef');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateHOTP', () => {
|
||||
it('generates HOTP codes for a given counter', () => {
|
||||
const key = 'JBSWY3DPEHPK3PXP';
|
||||
const hotpCodes = ['282760', '996554', '602287', '143627', '960129'];
|
||||
|
||||
for (const [counter, code] of hotpCodes.entries()) {
|
||||
expect(generateHOTP({ key, counter })).to.eql(code);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyHOTP', () => {
|
||||
it('validate hotp for a given secret', () => {
|
||||
const key = 'JBSWY3DPEHPK3PXP';
|
||||
const hotpCodes = ['282760', '996554', '602287', '143627', '960129'];
|
||||
|
||||
for (const [counter, token] of hotpCodes.entries()) {
|
||||
expect(verifyHOTP({ token, key, counter, window: 0 })).to.eql(true);
|
||||
}
|
||||
|
||||
expect(verifyHOTP({ token: 'INVALID', key })).to.eql(false);
|
||||
});
|
||||
|
||||
it('does not validate hotp out of sync', () => {
|
||||
const key = 'JBSWY3DPEHPK3PXP';
|
||||
const token = '282760';
|
||||
|
||||
expect(verifyHOTP({ token, key, counter: 5, window: 2 })).to.eql(false);
|
||||
expect(verifyHOTP({ token, key, counter: 5, window: 5 })).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateTOTP', () => {
|
||||
it('generates TOTP codes', () => {
|
||||
const key = 'JBSWY3DPEHPK3PXP';
|
||||
|
||||
const codes = [
|
||||
{ token: '282760', now: 0 },
|
||||
{ token: '341128', now: 1465324707000 },
|
||||
{ token: '089029', now: 1365324707000 },
|
||||
];
|
||||
|
||||
for (const { token, now } of codes) {
|
||||
expect(generateTOTP({ key, now })).to.eql(token);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyTOTP', () => {
|
||||
it('verify TOTP in sync codes against a key', () => {
|
||||
const key = 'JBSWY3DPEHPK3PXP';
|
||||
|
||||
const codes = [
|
||||
{ token: '282760', now: 0 },
|
||||
{ token: '341128', now: 1465324707000 },
|
||||
{ token: '089029', now: 1365324707000 },
|
||||
];
|
||||
|
||||
for (const { token, now } of codes) {
|
||||
expect(verifyTOTP({ key, token, now })).to.eql(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('does not validate totp out of sync', () => {
|
||||
const key = 'JBSWY3DPEHPK3PXP';
|
||||
const token = '635183';
|
||||
const now = 1661266455000;
|
||||
|
||||
expect(verifyTOTP({ key, token, now, window: 2 })).to.eql(true);
|
||||
expect(verifyTOTP({ key, token, now, window: 1 })).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildKeyUri', () => {
|
||||
it('build a key uri string', () => {
|
||||
expect(buildKeyUri({ secret: 'JBSWY3DPEHPK3PXP' })).to.eql(
|
||||
'otpauth://totp/IT-Tools:demo-user?issuer=IT-Tools&secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30',
|
||||
);
|
||||
|
||||
expect(
|
||||
buildKeyUri({
|
||||
secret: 'JBSWY3DPEHPK3PXP',
|
||||
app: 'app-name',
|
||||
account: 'account',
|
||||
algorithm: 'algo',
|
||||
digits: 7,
|
||||
period: 10,
|
||||
}),
|
||||
).to.eql(
|
||||
'otpauth://totp/app-name:account?issuer=app-name&secret=JBSWY3DPEHPK3PXP&algorithm=algo&digits=7&period=10',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
139
src/tools/otp-code-generator-and-validator/otp.service.ts
Normal file
139
src/tools/otp-code-generator-and-validator/otp.service.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { enc, HmacSHA1 } from 'crypto-js';
|
||||
import _ from 'lodash';
|
||||
import { createToken } from '../token-generator/token-generator.service';
|
||||
|
||||
export {
|
||||
generateHOTP,
|
||||
hexToBytes,
|
||||
verifyHOTP,
|
||||
generateTOTP,
|
||||
verifyTOTP,
|
||||
buildKeyUri,
|
||||
generateSecret,
|
||||
base32toHex,
|
||||
getCounterFromTime,
|
||||
};
|
||||
|
||||
function hexToBytes(hex: string) {
|
||||
return (hex.match(/.{1,2}/g) ?? []).map((char) => parseInt(char, 16));
|
||||
}
|
||||
|
||||
function computeHMACSha1(message: string, key: string) {
|
||||
return HmacSHA1(enc.Hex.parse(message), enc.Hex.parse(base32toHex(key))).toString(enc.Hex);
|
||||
}
|
||||
|
||||
function base32toHex(base32: string) {
|
||||
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
|
||||
const bits = base32
|
||||
.replace(/=+$/, '')
|
||||
.split('')
|
||||
.map((value) => base32Chars.indexOf(value).toString(2).padStart(5, '0'))
|
||||
.join('');
|
||||
|
||||
const hex = (bits.match(/.{1,8}/g) ?? []).map((chunk) => parseInt(chunk, 2).toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return hex;
|
||||
}
|
||||
|
||||
function generateHOTP({ key, counter = 0 }: { key: string; counter?: number }) {
|
||||
// Compute HMACdigest
|
||||
const digest = computeHMACSha1(counter.toString(16).padStart(16, '0'), key);
|
||||
|
||||
// Get byte array
|
||||
const bytes = hexToBytes(digest);
|
||||
|
||||
// Truncate
|
||||
const offset = bytes[19] & 0xf;
|
||||
const v =
|
||||
((bytes[offset] & 0x7f) << 24) |
|
||||
((bytes[offset + 1] & 0xff) << 16) |
|
||||
((bytes[offset + 2] & 0xff) << 8) |
|
||||
(bytes[offset + 3] & 0xff);
|
||||
|
||||
const code = String(v % 1000000).padStart(6, '0');
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
function verifyHOTP({
|
||||
token,
|
||||
key,
|
||||
window = 0,
|
||||
counter = 0,
|
||||
}: {
|
||||
token: string;
|
||||
key: string;
|
||||
window?: number;
|
||||
counter?: number;
|
||||
}) {
|
||||
for (let i = counter - window; i <= counter + window; ++i) {
|
||||
if (generateHOTP({ key, counter: i }) === token) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getCounterFromTime({ now, timeStep }: { now: number; timeStep: number }) {
|
||||
return Math.floor(now / 1000 / timeStep);
|
||||
}
|
||||
|
||||
function generateTOTP({ key, now = Date.now(), timeStep = 30 }: { key: string; now?: number; timeStep?: number }) {
|
||||
const counter = getCounterFromTime({ now, timeStep });
|
||||
|
||||
return generateHOTP({ key, counter });
|
||||
}
|
||||
|
||||
function verifyTOTP({
|
||||
key,
|
||||
token,
|
||||
window = 0,
|
||||
now = Date.now(),
|
||||
timeStep = 30,
|
||||
}: {
|
||||
token: string;
|
||||
key: string;
|
||||
window?: number;
|
||||
now?: number;
|
||||
timeStep?: number;
|
||||
}) {
|
||||
const counter = getCounterFromTime({ now, timeStep });
|
||||
|
||||
return verifyHOTP({ token, key, window, counter });
|
||||
}
|
||||
|
||||
function buildKeyUri({
|
||||
secret,
|
||||
app = 'IT-Tools',
|
||||
account = 'demo-user',
|
||||
algorithm = 'SHA1',
|
||||
digits = 6,
|
||||
period = 30,
|
||||
}: {
|
||||
secret: string;
|
||||
app?: string;
|
||||
account?: string;
|
||||
algorithm?: string;
|
||||
digits?: number;
|
||||
period?: number;
|
||||
}) {
|
||||
const params = {
|
||||
issuer: app,
|
||||
secret,
|
||||
algorithm,
|
||||
digits,
|
||||
period,
|
||||
};
|
||||
|
||||
const paramsString = _(params)
|
||||
.map((value, key) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join('&');
|
||||
|
||||
return `otpauth://totp/${encodeURIComponent(app)}:${encodeURIComponent(account)}?${paramsString}`;
|
||||
}
|
||||
|
||||
function generateSecret() {
|
||||
return createToken({ length: 16, alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' });
|
||||
}
|
66
src/tools/otp-code-generator-and-validator/token-display.vue
Normal file
66
src/tools/otp-code-generator-and-validator/token-display.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-space class="labels" item-style="flex: 1 1 0" style="width: 100%" align="center">
|
||||
<div style="text-align: left">Previous</div>
|
||||
<div style="text-align: center">Current OTP</div>
|
||||
<div style="text-align: right">Next</div>
|
||||
</n-space>
|
||||
<n-input-group>
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<n-button secondary @click.prevent="copyPrevious(tokens.previous)">{{ tokens.previous }}</n-button>
|
||||
</template>
|
||||
<div>{{ previousCopied ? 'Copied !' : 'Copy previous OTP' }}</div>
|
||||
</n-tooltip>
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="primary" class="current-otp" @click.prevent="copyCurrent(tokens.current)">
|
||||
{{ tokens.current }}
|
||||
</n-button>
|
||||
</template>
|
||||
<div>{{ currentCopied ? 'Copied !' : 'Copy current OTP' }}</div>
|
||||
</n-tooltip>
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<n-button secondary @click.prevent="copyNext(tokens.next)">{{ tokens.next }}</n-button>
|
||||
</template>
|
||||
<div>{{ nextCopied ? 'Copied !' : 'Copy next OTP' }}</div>
|
||||
</n-tooltip>
|
||||
</n-input-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { toRefs } from 'vue';
|
||||
|
||||
const { copy: copyPrevious, copied: previousCopied } = useClipboard();
|
||||
const { copy: copyCurrent, copied: currentCopied } = useClipboard();
|
||||
const { copy: copyNext, copied: nextCopied } = useClipboard();
|
||||
|
||||
const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>();
|
||||
const { tokens } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.current-otp {
|
||||
font-size: 22px;
|
||||
flex: 1 0 35% !important;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.labels {
|
||||
div {
|
||||
text-align: center;
|
||||
padding: 0 2px 6px 2px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
}
|
||||
|
||||
.n-input-group > * {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
@@ -32,9 +32,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
import { useQRCode } from './useQRCode';
|
||||
import { ref } from 'vue';
|
||||
import type { QRCodeErrorCorrectionLevel } from 'qrcode';
|
||||
import { useQRCode } from './useQRCode';
|
||||
|
||||
const foreground = ref('#000000ff');
|
||||
const background = ref('#ffffffff');
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { get, type MaybeRef } from '@vueuse/core';
|
||||
import QRCode, { type QRCodeErrorCorrectionLevel, type QRCodeToDataURLOptions } from 'qrcode';
|
||||
import { ref, watch, type Ref } from 'vue';
|
||||
import { ref, watch, isRef } from 'vue';
|
||||
|
||||
export function useQRCode({
|
||||
text,
|
||||
@@ -7,24 +8,24 @@ export function useQRCode({
|
||||
errorCorrectionLevel,
|
||||
options,
|
||||
}: {
|
||||
text: Ref<string>;
|
||||
color: { foreground: Ref<string>; background: Ref<string> };
|
||||
errorCorrectionLevel: Ref<QRCodeErrorCorrectionLevel>;
|
||||
text: MaybeRef<string>;
|
||||
color: { foreground: MaybeRef<string>; background: MaybeRef<string> };
|
||||
errorCorrectionLevel?: MaybeRef<QRCodeErrorCorrectionLevel>;
|
||||
options?: QRCodeToDataURLOptions;
|
||||
}) {
|
||||
const qrcode = ref('');
|
||||
|
||||
watch(
|
||||
[text, background, foreground, errorCorrectionLevel],
|
||||
[text, background, foreground, errorCorrectionLevel].filter(isRef),
|
||||
async () => {
|
||||
if (text.value)
|
||||
qrcode.value = await QRCode.toDataURL(text.value, {
|
||||
if (get(text))
|
||||
qrcode.value = await QRCode.toDataURL(get(text), {
|
||||
color: {
|
||||
dark: foreground.value,
|
||||
light: background.value,
|
||||
dark: get(foreground),
|
||||
light: get(background),
|
||||
...options?.color,
|
||||
},
|
||||
errorCorrectionLevel: errorCorrectionLevel.value,
|
||||
errorCorrectionLevel: get(errorCorrectionLevel) ?? 'M',
|
||||
...options,
|
||||
});
|
||||
},
|
||||
|
26
src/tools/sql-prettify/index.ts
Normal file
26
src/tools/sql-prettify/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Database } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'SQL prettify and format',
|
||||
path: '/sql-prettify',
|
||||
description: 'Format and prettify your SQL queries online (it supports various SQL dialects).',
|
||||
keywords: [
|
||||
'sql',
|
||||
'prettify',
|
||||
'beautify',
|
||||
'GCP BigQuery',
|
||||
'IBM DB2',
|
||||
'Apache Hive',
|
||||
'MariaDB',
|
||||
'MySQL',
|
||||
'Couchbase N1QL',
|
||||
'Oracle PL/SQL',
|
||||
'PostgreSQL',
|
||||
'Amazon Redshift',
|
||||
'Spark',
|
||||
'SQL Server Transact-SQL',
|
||||
],
|
||||
component: () => import('./sql-prettify.vue'),
|
||||
icon: Database,
|
||||
});
|
94
src/tools/sql-prettify/sql-prettify.vue
Normal file
94
src/tools/sql-prettify/sql-prettify.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div style="flex: 0 0 100%">
|
||||
<n-space item-style="flex:1 1 0" style="margin: 0 auto; max-width: 600px" :vertical="styleStore.isSmallScreen">
|
||||
<n-form-item label="Dialect" label-width="500">
|
||||
<n-select
|
||||
v-model:value="config.language"
|
||||
:options="[
|
||||
{ label: 'GCP BigQuery', value: 'bigquery' },
|
||||
{ label: 'IBM DB2', value: 'db2' },
|
||||
{ label: 'Apache Hive', value: 'hive' },
|
||||
{ label: 'MariaDB', value: 'mariadb' },
|
||||
{ label: 'MySQL', value: 'mysql' },
|
||||
{ label: 'Couchbase N1QL', value: 'n1ql' },
|
||||
{ label: 'Oracle PL/SQL', value: 'plsql' },
|
||||
{ label: 'PostgreSQL', value: 'postgresql' },
|
||||
{ label: 'Amazon Redshift', value: 'redshift' },
|
||||
{ label: 'Spark', value: 'spark' },
|
||||
{ label: 'Standard SQL', value: 'sql' },
|
||||
{ label: 'sqlite', value: 'sqlite' },
|
||||
{ label: 'SQL Server Transact-SQL', value: 'tsql' },
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Keyword case">
|
||||
<n-select
|
||||
v-model:value="config.keywordCase"
|
||||
:options="[
|
||||
{ label: 'UPPERCASE', value: 'upper' },
|
||||
{ label: 'lowercase', value: 'lower' },
|
||||
{ label: 'Preserve', value: 'preserve' },
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Indent style">
|
||||
<n-select
|
||||
v-model:value="config.indentStyle"
|
||||
:options="[
|
||||
{ label: 'Standard', value: 'standard' },
|
||||
{ label: 'Tabular left', value: 'tabularLeft' },
|
||||
{ label: 'Tabular right', value: 'tabularRight' },
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<n-form-item label="Your SQL query">
|
||||
<n-input
|
||||
ref="inputElement"
|
||||
v-model:value="rawSQL"
|
||||
placeholder="Put your SQL query here..."
|
||||
type="textarea"
|
||||
rows="20"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Prettify version of your query">
|
||||
<textarea-copyable :value="prettySQL" language="sql" :follow-height-of="inputElement" />
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
import { format as formatSQL, type FormatFnOptions } from 'sql-formatter';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
|
||||
const inputElement = ref<HTMLElement>();
|
||||
const styleStore = useStyleStore();
|
||||
const config = reactive<Partial<FormatFnOptions>>({
|
||||
keywordCase: 'upper',
|
||||
useTabs: false,
|
||||
language: 'sql',
|
||||
indentStyle: 'standard',
|
||||
tabulateAlias: true,
|
||||
});
|
||||
|
||||
const rawSQL = ref('select field1,field2,field3 from my_table where my_condition;');
|
||||
const prettySQL = computed(() => formatSQL(rawSQL.value, config));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.result-card {
|
||||
position: relative;
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
11
src/tools/svg-placeholder-generator/index.ts
Normal file
11
src/tools/svg-placeholder-generator/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ImageOutlined } from '@vicons/material';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'SVG placeholder generator',
|
||||
path: '/svg-placeholder-generator',
|
||||
description: 'Generate svg images to use as placeholder in your applications.',
|
||||
keywords: ['svg', 'placeholder', 'generator', 'image', 'size', 'mockup'],
|
||||
component: () => import('./svg-placeholder-generator.vue'),
|
||||
icon: ImageOutlined,
|
||||
});
|
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-form label-placement="left" label-width="100">
|
||||
<n-space item-style="flex:1 1 0">
|
||||
<n-form-item label="Width (in px)">
|
||||
<n-input-number v-model:value="width" placeholder="SVG width..." min="1" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Background">
|
||||
<n-color-picker v-model:value="bgColor" :modes="['hex']" />
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
<n-space item-style="flex:1 1 0">
|
||||
<n-form-item label="Height (in px)">
|
||||
<n-input-number v-model:value="height" placeholder="SVG height..." min="1" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Text color">
|
||||
<n-color-picker v-model:value="fgColor" :modes="['hex']" />
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
<n-space item-style="flex:1 1 0">
|
||||
<n-form-item label="Font size">
|
||||
<n-input-number v-model:value="fontSize" placeholder="Font size..." min="1" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Custom text">
|
||||
<n-input v-model:value="customText" :placeholder="`Default is ${width}x${height}`" />
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
<n-form-item label="Use exact size" label-placement="left">
|
||||
<n-switch v-model:value="useExactSize" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<n-form-item label="SVG HTML element">
|
||||
<textarea-copyable :value="svgString" copy-placement="none" />
|
||||
</n-form-item>
|
||||
<n-form-item label="SVG in Base64">
|
||||
<textarea-copyable :value="base64" copy-placement="none" />
|
||||
</n-form-item>
|
||||
|
||||
<n-space justify="center">
|
||||
<n-button secondary @click="copySVG()">Copy svg</n-button>
|
||||
<n-button secondary @click="copyBase64()">Copy base64</n-button>
|
||||
<n-button secondary @click="download()">Download svg</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<n-space vertical justify="start">
|
||||
<img :src="base64" alt="Image" />
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
import { textToBase64 } from '@/utils/base64';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const width = ref(600);
|
||||
const height = ref(350);
|
||||
const fontSize = ref(26);
|
||||
const bgColor = ref('#cccccc');
|
||||
const fgColor = ref('#333333');
|
||||
const useExactSize = ref(true);
|
||||
const customText = ref('');
|
||||
const svgString = computed(() => {
|
||||
const w = width.value;
|
||||
const h = height.value;
|
||||
const text = customText.value.length > 0 ? customText.value : `${w}x${h}`;
|
||||
const size = useExactSize.value ? ` width="${w}" height="${h}"` : '';
|
||||
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}"${size}>
|
||||
<rect width="${w}" height="${h}" fill="${bgColor.value}"></rect>
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="monospace" font-size="${fontSize.value}px" fill="${fgColor.value}">${text}</text>
|
||||
</svg>
|
||||
`.trim();
|
||||
});
|
||||
const base64 = computed(() => 'data:image/svg+xml;base64,' + textToBase64(svgString.value));
|
||||
|
||||
const { copy: copySVG } = useCopy({ source: svgString });
|
||||
const { copy: copyBase64 } = useCopy({ source: base64 });
|
||||
const { download } = useDownloadFileFromBase64({ source: base64 });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@@ -5,19 +5,17 @@
|
||||
<br />
|
||||
<n-space justify="space-around">
|
||||
<n-statistic label="Character count" :value="text.length" />
|
||||
<n-statistic label="Word count" :value="text.split(/\s+/).length" />
|
||||
<n-statistic label="Line count" :value="text.split(/\r\n|\r|\n/).length" />
|
||||
<n-statistic label="Word count" :value="text === '' ? 0 : text.split(/\s+/).length" />
|
||||
<n-statistic label="Line count" :value="text === '' ? 0 : text.split(/\r\n|\r|\n/).length" />
|
||||
<n-statistic label="Byte size" :value="formatBytes(getStringSizeInBytes(text))" />
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { formatBytes } from '@/utils/convert';
|
||||
import { ref } from 'vue';
|
||||
import { getStringSizeInBytes } from './text-statistics.service';
|
||||
|
||||
const text = ref(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Commodo risus faucibus varius volutpat habitasse suspendisse justo inceptos primis mi. Fusce molestie lorem bibendum habitasse litora adipiscing turpis egestas quis nec. Non id conubia vulputate etiam iaculis vitae venenatis hac fusce condimentum. Adipiscing pellentesque venenatis ornare pulvinar tempus hac montes velit erat convallis.',
|
||||
);
|
||||
const text = ref('');
|
||||
</script>
|
||||
|
@@ -6,19 +6,23 @@ export function createToken({
|
||||
withNumbers = true,
|
||||
withSymbols = false,
|
||||
length = 64,
|
||||
alphabet,
|
||||
}: {
|
||||
withUppercase?: boolean;
|
||||
withLowercase?: boolean;
|
||||
withNumbers?: boolean;
|
||||
withSymbols?: boolean;
|
||||
length?: number;
|
||||
alphabet?: string;
|
||||
}) {
|
||||
const alphabet = [
|
||||
...(withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : ''),
|
||||
...(withLowercase ? 'abcdefghijklmopqrstuvwxyz' : ''),
|
||||
...(withNumbers ? '0123456789' : ''),
|
||||
...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : ''),
|
||||
].join('');
|
||||
const allAlphabet =
|
||||
alphabet ??
|
||||
[
|
||||
...(withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : ''),
|
||||
...(withLowercase ? 'abcdefghijklmopqrstuvwxyz' : ''),
|
||||
...(withNumbers ? '0123456789' : ''),
|
||||
...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : ''),
|
||||
].join('');
|
||||
|
||||
return shuffleString(alphabet.repeat(length)).substring(0, length);
|
||||
return shuffleString(allAlphabet.repeat(length)).substring(0, length);
|
||||
}
|
||||
|
@@ -60,29 +60,18 @@
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { isNotThrowing } from '@/utils/boolean';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const encodeInput = ref('Hello world :)');
|
||||
const encodeOutput = computed(() => {
|
||||
try {
|
||||
return encodeURIComponent(encodeInput.value);
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
const encodeOutput = computed(() => withDefaultOnError(() => encodeURIComponent(encodeInput.value), ''));
|
||||
|
||||
const encodedValidation = useValidation({
|
||||
source: encodeInput,
|
||||
rules: [
|
||||
{
|
||||
validator: (value) => {
|
||||
try {
|
||||
encodeURIComponent(value);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
validator: (value) => isNotThrowing(() => encodeURIComponent(value)),
|
||||
message: 'Impossible to parse this string',
|
||||
},
|
||||
],
|
||||
@@ -91,27 +80,13 @@ const encodedValidation = useValidation({
|
||||
const { copy: copyEncoded } = useCopy({ source: encodeOutput, text: 'Encoded string copied to the clipboard' });
|
||||
|
||||
const decodeInput = ref('Hello%20world%20%3A)');
|
||||
|
||||
const decodeOutput = computed(() => {
|
||||
try {
|
||||
return decodeURIComponent(decodeInput.value);
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
const decodeOutput = computed(() => withDefaultOnError(() => decodeURIComponent(decodeInput.value), ''));
|
||||
|
||||
const decodeValidation = useValidation({
|
||||
source: encodeInput,
|
||||
rules: [
|
||||
{
|
||||
validator: (value) => {
|
||||
try {
|
||||
decodeURIComponent(value);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
validator: (value) => isNotThrowing(() => decodeURIComponent(value)),
|
||||
message: 'Impossible to parse this string',
|
||||
},
|
||||
],
|
||||
|
@@ -27,31 +27,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { SubdirectoryArrowRightRound } from '@vicons/material';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { isNotThrowing } from '@/utils/boolean';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { SubdirectoryArrowRightRound } from '@vicons/material';
|
||||
import { computed, ref } from 'vue';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
|
||||
const urlToParse = ref('https://me:pwd@it-tools.tech:3000/url-parser?key1=value&key2=value2#the-hash');
|
||||
const urlParsed = computed<URL | undefined>(() => {
|
||||
try {
|
||||
return new URL(urlToParse.value);
|
||||
} catch (_) {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const urlParsed = computed(() => withDefaultOnError(() => new URL(urlToParse.value), undefined));
|
||||
const validation = useValidation({
|
||||
source: urlToParse,
|
||||
rules: [
|
||||
{
|
||||
validator: (value) => {
|
||||
try {
|
||||
new URL(value);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
validator: (value) => isNotThrowing(() => new URL(value)),
|
||||
message: 'Invalid url',
|
||||
},
|
||||
],
|
||||
|
69
src/utils/base64.test.ts
Normal file
69
src/utils/base64.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { base64ToText, isValidBase64, removePotentialDataAndMimePrefix, textToBase64 } from './base64';
|
||||
|
||||
describe('base64 utils', () => {
|
||||
describe('textToBase64', () => {
|
||||
it('should convert string into base64', () => {
|
||||
expect(textToBase64('')).to.eql('');
|
||||
expect(textToBase64('a')).to.eql('YQ==');
|
||||
expect(textToBase64('lorem ipsum')).to.eql('bG9yZW0gaXBzdW0=');
|
||||
expect(textToBase64('-1')).to.eql('LTE=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('base64ToText', () => {
|
||||
it('should convert base64 into text', () => {
|
||||
expect(base64ToText('')).to.eql('');
|
||||
expect(base64ToText('YQ==')).to.eql('a');
|
||||
expect(base64ToText('bG9yZW0gaXBzdW0=')).to.eql('lorem ipsum');
|
||||
expect(base64ToText('data:text/plain;base64,bG9yZW0gaXBzdW0=')).to.eql('lorem ipsum');
|
||||
expect(base64ToText('LTE=')).to.eql('-1');
|
||||
});
|
||||
|
||||
it('should throw for incorrect base64 string', () => {
|
||||
expect(() => base64ToText('a')).to.throw('Incorrect base64 string');
|
||||
expect(() => base64ToText(' ')).to.throw('Incorrect base64 string');
|
||||
expect(() => base64ToText('é')).to.throw('Incorrect base64 string');
|
||||
// missing final '='
|
||||
expect(() => base64ToText('bG9yZW0gaXBzdW0')).to.throw('Incorrect base64 string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidBase64', () => {
|
||||
it('should return true for correct base64 string', () => {
|
||||
expect(isValidBase64('')).to.eql(true);
|
||||
expect(isValidBase64('bG9yZW0gaXBzdW0=')).to.eql(true);
|
||||
expect(isValidBase64('LTE=')).to.eql(true);
|
||||
expect(isValidBase64('YQ==')).to.eql(true);
|
||||
expect(isValidBase64('data:text/plain;base64,YQ==')).to.eql(true);
|
||||
});
|
||||
|
||||
it('should return false for incorrect base64 string', () => {
|
||||
expect(isValidBase64('a')).to.eql(false);
|
||||
expect(isValidBase64(' ')).to.eql(false);
|
||||
expect(isValidBase64('é')).to.eql(false);
|
||||
expect(isValidBase64('data:text/plain;notbase64,YQ==')).to.eql(false);
|
||||
// missing final '='
|
||||
expect(isValidBase64('bG9yZW0gaXBzdW0')).to.eql(false);
|
||||
});
|
||||
|
||||
it('should return false for untrimmed correct base64 string', () => {
|
||||
expect(isValidBase64('bG9yZW0gaXBzdW0= ')).to.eql(false);
|
||||
expect(isValidBase64(' LTE=')).to.eql(false);
|
||||
expect(isValidBase64(' YQ== ')).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removePotentialDataAndMimePrefix', () => {
|
||||
it('should remove data prefix of string', () => {
|
||||
expect(removePotentialDataAndMimePrefix('')).to.eql('');
|
||||
expect(removePotentialDataAndMimePrefix('lorem ipsum')).to.eql('lorem ipsum');
|
||||
expect(removePotentialDataAndMimePrefix('bG9yZW0gaXBzdW0=')).to.eql('bG9yZW0gaXBzdW0=');
|
||||
expect(removePotentialDataAndMimePrefix('data:image/jpeg;base64,lorem')).to.eql('lorem');
|
||||
expect(removePotentialDataAndMimePrefix('data:image/jpeg;notbase64,lorem')).to.eql(
|
||||
'data:image/jpeg;notbase64,lorem',
|
||||
);
|
||||
expect(removePotentialDataAndMimePrefix('data:unknownmime;base64,lorem')).to.eql('lorem');
|
||||
});
|
||||
});
|
||||
});
|
33
src/utils/base64.ts
Normal file
33
src/utils/base64.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export { textToBase64, base64ToText, isValidBase64, removePotentialDataAndMimePrefix };
|
||||
|
||||
function textToBase64(str: string) {
|
||||
return window.btoa(str);
|
||||
}
|
||||
|
||||
function base64ToText(str: string) {
|
||||
if (!isValidBase64(str)) {
|
||||
throw new Error('Incorrect base64 string');
|
||||
}
|
||||
|
||||
const cleanStr = removePotentialDataAndMimePrefix(str);
|
||||
|
||||
try {
|
||||
return window.atob(cleanStr);
|
||||
} catch (_) {
|
||||
throw new Error('Incorrect base64 string');
|
||||
}
|
||||
}
|
||||
|
||||
function removePotentialDataAndMimePrefix(str: string) {
|
||||
return str.replace(/^data:.*?;base64,/, '');
|
||||
}
|
||||
|
||||
function isValidBase64(str: string) {
|
||||
const cleanStr = removePotentialDataAndMimePrefix(str);
|
||||
|
||||
try {
|
||||
return window.btoa(window.atob(cleanStr)) === cleanStr;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user