Compare commits

...

200 Commits

Author SHA1 Message Date
Corentin Thomasset
670f735501 chore(version): release 2023.12.21-5ed3693 2023-12-21 08:45:28 +01:00
Corentin Thomasset
a29ad66809 docs(changelog): update changelog for 2023.12.21-5ed3693 2023-12-21 08:45:27 +01:00
Markus Blaschke
5ed36935c7 fix(jwt-parser): jwt claim array support (#799)
fixes support of claim array values (was shown "[Object]" before)

Signed-off-by: Markus Blaschke <mblaschke82@gmail.com>
2023-12-12 14:04:19 +01:00
Isaiah
80e46c9292 fix(camera-recorder): stop camera on navigation (#782)
* Stopping camera when user switches to another page

* Missing ;
2023-11-29 13:58:07 +01:00
Richard "EpicKitty" Bowey
7a70dbbe0c fix(doc): updated create new tool command in readme (#762) 2023-11-22 20:27:46 +01:00
lzb16
2e56641398 feat(i18n): improve chinese i18n (#757) 2023-11-16 18:27:51 +01:00
lzb16
a1037cf8f1 feat(i18n): add tooltip and favoriteButton i18n (#756) 2023-11-16 08:20:42 +00:00
renovate[bot]
0fe9a20329 chore(deps): switched from oui to oui-data for mac address lookup (#693)
* fix(deps): update dependency oui to v13

* chore(deps): switched from oui to oui-data for mac address lookup

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2023-11-14 23:44:23 +00:00
renovate[bot]
2e396d8776 chore(deps): update unocss monorepo to ^0.57.0 (#638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 00:19:09 +01:00
Corentin THOMASSET
bd3edcb528 refactor(about, i18n): improved i18n dx with markdown (#753) 2023-11-14 23:15:41 +00:00
Corentin THOMASSET
de1ee69ef9 refactor(token, i18n): complete fr translation (#752) 2023-11-14 23:14:05 +00:00
Gary
8f99eb6017 feat(i18n): add Chinese translation base (#718)
* Create zh_CN.yml

* rename

* Update zh.yml

* Update zh.yml

* Update zh.yml

* Update zh.yml

* Update zh.yml
2023-11-15 00:09:36 +01:00
Corentin THOMASSET
38586caab7 refactor(uuid generator): uuid version picker (#751) 2023-11-13 21:38:01 +00:00
Corentin THOMASSET
043e4f0a08 fix(base64-file-converter): fix downloading of index.html content without data preambula (#750)
* fix(base64-file-converter): fix downloading of index.html content without data preambula

* feat(base64-file-converter): infer mime type from base64 signature

---------

Co-authored-by: akharlov <harl_aa@skbkontur.ru>
2023-11-13 10:44:23 +00:00
Corentin THOMASSET
ca43a25569 refactor(case converter): no split on lowercase, uppercase and mocking case (#748) 2023-11-13 00:00:55 +00:00
Corentin THOMASSET
7fe47b3be4 refactor(ui): replaced legacy n-upload with c-file-upload (#747) 2023-11-13 00:56:51 +01:00
Corentin THOMASSET
16ffe6b5c9 refactor(token): added password in token generator keywords (#746) 2023-11-12 22:54:13 +00:00
Corentin THOMASSET
478192065e feat(new tool): pdf signature checker (#745) 2023-11-12 22:22:41 +00:00
Corentin THOMASSET
205e360400 fix(docker): rollback armv7 in docker releases (#741) 2023-11-09 13:33:19 +00:00
Steltek
821cbea2bf fix(eta): corrected example (#737) 2023-11-08 19:56:58 +01:00
Oceansdeep7
093ff311fd refactor(bcrypt): fix input label align (#721)
Co-authored-by: robin.li <robin.li@webeye.com>
2023-11-07 09:10:52 +01:00
Corentin THOMASSET
e07e2ae5bc feat(new tool): numeronym generator (#729) 2023-11-05 21:59:31 +00:00
Corentin THOMASSET
fe1de8c5c9 chore(docker): added armv7 plateform for docker releases (#722) 2023-11-03 08:06:11 +00:00
Corentin Thomasset
b2614990e3 chore(version): release 2023.11.02-7d94e11 2023-11-02 09:11:26 +01:00
Corentin Thomasset
79646375f3 docs(changelog): update changelog for 2023.11.02-7d94e11 2023-11-02 09:11:26 +01:00
Andrii Bratanin
7d94e11cee fix(dockerfile): revert replacement of nginx image with non-privileged one (#716)
Fixes https://github.com/CorentinTh/it-tools/issues/714
2023-11-02 09:10:13 +01:00
swishkin
e87f4b1837 refactor(math-evaluator): improved description
Improved description.
2023-11-01 23:07:51 +01:00
Corentin THOMASSET
e86fd96ae3 feat(i18n): language selector (#710) 2023-11-01 15:38:19 +01:00
Corentin THOMASSET
58de8970f5 refactor(math-evaluator): improved search and UX (#713)
* refactor(math-evaluator): added keywords for search

* refactor(math-evaluator): improved input
2023-11-01 14:23:46 +00:00
Corentin THOMASSET
02b0d0d1a1 fix(encryption): alert on decryption error (#711)
* update(c-alert): Add variant 'error'

* fix(encryption): Alert decryption error (#652)

* feat(c-alert): added title

* refactor(composable): mutualized computedCatch

---------

Co-authored-by: code2933 <code2933@outlook.com>
2023-11-01 10:11:51 +00:00
Corentin Thomasset
4d5a67d96d chore(version): release 2023.11.01-e164afb 2023-11-01 10:02:22 +01:00
Corentin Thomasset
8174db9cf3 docs(changelog): update changelog for 2023.11.01-e164afb 2023-11-01 10:02:22 +01:00
Corentin THOMASSET
e164afb664 chore(deps): clean unused dependencies (#709)
* chore(deps): removed unused dep randombytes

* chore(deps): removed unused dep c8

* chore(deps): updated dep naive-ui
2023-10-31 21:00:23 +00:00
Corentin THOMASSET
d0136962b9 feat(command-palette): clear prompt on palette close (#708) 2023-10-31 19:27:21 +00:00
Corentin THOMASSET
015c673e09 refactor(ui): better ui demo preview menu (#664) 2023-10-31 17:49:36 +00:00
Corentin Thomasset
99b1eb944d feat(command-palette): added about page in command palette 2023-10-31 18:26:16 +01:00
Istvan Olah
cc3425dc77 feat(new tool): random MAC address generator (#657)
* #521 Random MAC address generator

* refactor(mac-address-generator): improved ux

* refactor(mac-address-generator): improved ux

---------

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
2023-10-31 11:34:24 +00:00
Corentin THOMASSET
681f7bf644 feat(case-converter): added mocking case (#705) 2023-10-31 09:52:01 +00:00
Corentin THOMASSET
f5eb7a8c49 feat(date-converter): added excel date time format (#704) 2023-10-31 08:59:35 +00:00
Corentin THOMASSET
abb8335041 refactor(color-converter): improved color-converter UX (#701) 2023-10-31 07:32:44 +00:00
Corentin THOMASSET
020e9cbe41 refactor(docker): improved docker config (#700) 2023-10-30 17:11:16 +00:00
莫颓
02e68d3f56 feat(i18n): token generator (#688) 2023-10-23 09:17:08 +02:00
莫颓
00562ed5e8 feat(i18n): home page (#687)
(cherry picked from commit 9d39826078ceb929a5ca3b577f9f39449303c289)
2023-10-23 09:14:34 +02:00
renovate[bot]
4365226d01 chore(deps): update docker/setup-qemu-action action to v3 (#627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-22 10:30:26 +02:00
renovate[bot]
57ecda1623 chore(deps): update docker/setup-buildx-action action to v3 (#626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-22 10:30:16 +02:00
renovate[bot]
d8d7a3b9ab chore(deps): update docker/login-action action to v3 (#625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-22 10:30:04 +02:00
renovate[bot]
d36b18f193 chore(deps): update docker/build-push-action action to v5 (#624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-22 10:29:05 +02:00
renovate[bot]
eea9f91276 chore(deps): update dependency node to v18.18.2 (#674)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-22 10:26:05 +02:00
莫颓
ebb4ec4165 feat(i18n): support for i18n in .ts files (#683)
(cherry picked from commit 732313bfc32a514ef064ca0f90304ff05e2e7ef3)
2023-10-22 10:23:00 +02:00
莫颓
84a4a646f6 feat(i18n): tool card (#682)
(cherry picked from commit 1d0a3904f72ab24364beda034d17cf6de9b5e959)
2023-10-22 08:21:10 +00:00
莫颓
a2b53c2e38 feat(i18n): about page (#680)
(cherry picked from commit 605b84c16e0eec1a16e6133203e7810a68447607)

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
2023-10-22 08:15:39 +00:00
莫颓
35563b8457 feat(i18n): 404 page (#679) 2023-10-22 10:07:43 +02:00
Sellar
720201aa7b fix(deps): fix issue on slugify (#593) (#673) 2023-10-18 09:23:34 +02:00
Corentin THOMASSET
b2ad4f7a27 feat(new tool): text to ascii converter (#669) 2023-10-15 22:57:47 +00:00
Corentin Thomasset
b408df82c1 refactor(c-table): added description on c-table for accessibility 2023-10-15 23:41:55 +02:00
Corentin THOMASSET
88b881880c refactor(ci): reduced timeout in e2e (#666) 2023-10-14 22:48:18 +00:00
Corentin THOMASSET
ee4c853b9f refactor(ui): new c-table ui component (#665) 2023-10-14 22:45:14 +00:00
Corentin THOMASSET
cbf58fdd28 refactor(ui): removed n-page-header component in user-agent parser (#663) 2023-10-14 23:01:32 +02:00
Corentin THOMASSET
a757a5155a refactor(ui): removed n-p components in about page (#662) 2023-10-14 18:37:26 +02:00
Corentin THOMASSET
025f556023 refactor(ui): switched naive tooltip components to custom ones (#661) 2023-10-14 16:24:54 +00:00
renovate[bot]
2d2dffb14a chore(deps): update dependency node to v18.18.0 (#636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-04 11:46:33 +02:00
Corentin THOMASSET
5c4d775e2d feat(new tool): ULID generator (#623) 2023-09-11 22:57:42 +00:00
Corentin THOMASSET
557b30426f doc(readme): added contributors list (#622) 2023-09-11 20:59:07 +00:00
renovate[bot]
4972159aa7 chore(deps): update actions/checkout action to v4 (#613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-11 22:42:33 +02:00
renovate[bot]
e371ef702e fix(deps): update dependency monaco-editor to ^0.43.0 (#620)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-11 22:40:42 +02:00
Simon Bordeyne
0eedce69a6 feat(new tool): add wifi qr code generator (#599)
* (feat: new tool): add wifi qr code generator

* Update src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue

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

* Update src/tools/wifi-qr-code-generator/index.ts

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

* remove naive UI grid

* Update src/tools/wifi-qr-code-generator/index.ts

---------

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
2023-09-06 07:46:40 +00:00
Mark Townsend
8a30b6bdb3 refactor(spelling): minor corrections to phrasing/spelling (#596)
* Minor corrections to phrasing/spelling.

* Corrected 'millennia'.

* Corrected tests.

---------

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
2023-09-04 14:51:04 +02:00
Corentin THOMASSET
233d5565f6 refactor(i18n): merge tools scoped locales with global ones (#612) 2023-09-03 20:07:45 +00:00
Corentin THOMASSET
7ab9204e96 refactor(c-key-value-list): got rid of table for layout (#611) 2023-09-03 16:12:22 +00:00
renovate[bot]
c7d4562d3b fix(deps): update dependency sql-formatter to v13 (#606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-03 16:51:12 +02:00
Corentin THOMASSET
18dd1400bd refactor(CI): run e2e against built app and no longer vercel (#610) 2023-09-03 14:28:19 +00:00
renovate[bot]
f035f485c0 chore(deps): update dependency unplugin-icons to ^0.17.0 (#609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-03 16:16:01 +02:00
Anton Mosich
e18bae1fca refactor(bcrypt): fix typo (#604) 2023-09-01 16:30:51 +02:00
renovate[bot]
d1dff428d8 chore(deps): update dependency @intlify/unplugin-vue-i18n to ^0.13.0 (#597)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-01 10:14:50 +02:00
Corentin THOMASSET
3a63837d3d feat(new tool): iban validation and parser (#591) 2023-08-27 18:12:31 +00:00
Corentin THOMASSET
81bfe57cb8 feat(new tool): text diff and comparator (#588)
* feat(new tool): text diff and comparator

* chore(ci): increased memory in CI
2023-08-26 16:43:47 +02:00
renovate[bot]
a9cd91ca9c chore(deps): update dependency @antfu/eslint-config to ^0.41.0 (#585)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-26 16:28:31 +02:00
Corentin THOMASSET
06c35472d3 doc(hosting): added cloudron in the other hosting solutions section (#589) 2023-08-26 14:18:12 +00:00
renovate[bot]
f3e14fc18f chore(deps): update dependency typescript to ~5.2.0 (#587)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-26 16:17:19 +02:00
Corentin Thomasset
2274766a8f chore(version): release 2023.08.21-6f93cba 2023-08-22 01:02:24 +02:00
Corentin Thomasset
a346175d24 docs(changelog): update changelog for 2023.08.21-6f93cba 2023-08-22 01:02:24 +02:00
Corentin THOMASSET
6f93cba3da feat(copy): support legacy copy to clipboard for older browser (#581) 2023-08-21 23:00:20 +00:00
renovate[bot]
76b2761d62 chore(deps): switched to fucking typescript v5 (#501)
* chore(deps): update dependency typescript to v5

* chore(deps): switched to fucking typescript v5

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2023-08-21 19:57:59 +00:00
renovate[bot]
6ff9a01cc8 chore(deps): update dependency @antfu/eslint-config to ^0.40.0 (#552)
* chore(deps): update dependency @antfu/eslint-config to ^0.40.0

* chore(deps): updated eslint

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2023-08-21 18:27:08 +00:00
renovate[bot]
a2b9b157e5 chore(deps): update dependency prettier to v3 (#564)
* chore(deps): update dependency prettier to v3

* chore(deps): updated prettier

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2023-08-21 18:02:54 +00:00
renovate[bot]
144f86e2dc chore(deps): removed @typescript-eslint/parser (#563)
* chore(deps): update dependency @typescript-eslint/parser to v6

* chore(deps): removed @typescript-eslint/parser

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2023-08-21 17:18:04 +00:00
renovate[bot]
0f1f6590c5 chore(deps): removed ts-pattern (#565)
* fix(deps): update dependency ts-pattern to v5

* chore(deps): removed ts-pattern

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2023-08-21 17:06:09 +00:00
renovate[bot]
2bcb77a9f9 fix(deps): update dependency sql-formatter to v12 (#520)
* fix(deps): update dependency sql-formatter to v12

* fix(sql-prettify): corrected types

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2023-08-21 18:34:15 +02:00
Corentin THOMASSET
c58d6e3423 feat(new tool): string obfuscator (#575) 2023-08-16 21:43:45 +00:00
Corentin Thomasset
f235dcd6c1 chore(version): release 2023.08.16-9bd4ad4 2023-08-16 14:19:08 +02:00
Corentin Thomasset
ba2c589f0f docs(changelog): update changelog for 2023.08.16-9bd4ad4 2023-08-16 14:19:08 +02:00
renovate[bot]
9bd4ad4dfd chore(deps): update dependency vitest to ^0.34.0 (#562)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-10 16:15:18 +02:00
renovate[bot]
5e12991bcd fix(deps): update dependency uuid to v9 (#566)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-10 15:12:27 +02:00
renovate[bot]
65a9474078 chore(deps): update dependency node to v18.17.1 (#560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-10 01:10:53 +02:00
renovate[bot]
85cc7a8447 chore(deps): update dependency unocss to ^0.55.0 (#561)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-09 22:31:31 +00:00
renovate[bot]
4268e255de chore(deps): update dependency @unocss/eslint-config to ^0.55.0 (#553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-09 22:22:15 +00:00
renovate[bot]
d1c888019b chore(deps): update dependency @intlify/unplugin-vue-i18n to ^0.12.0 (#526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-10 00:11:59 +02:00
renovate[bot]
7924456cec fix(deps): update dependency mathjs to v11 (#519)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-10 00:11:30 +02:00
renovate[bot]
99bc84c37e chore(deps): update docker/login-action action to v2 (#512)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-10 00:10:31 +02:00
renovate[bot]
ea0f27cf4c fix(deps): update dependency @vueuse/router to v10 (#516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-10 00:10:19 +02:00
renovate[bot]
cd5a503fc0 chore(deps): update dependency jsdom to v22 (#499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-10 00:09:39 +02:00
Corentin THOMASSET
86e964a274 fix(copy): prevent shorthand copy if source is present in useCopy (#559)
* fix(copy): prevent shorthand copy if source is present in useCopy

* refactor(copy): normalized copy usage
2023-08-09 22:07:44 +00:00
Marvin
7b6232a151 feat(Case Converter): Add lowercase and uppercase (#534)
* feat(case converter): add uppercase and lowercase

* (case converter) correctly use stripRegexp

* style: lint fix

* feat(ui): added c-select in the ui lib (#550)

* feat(ui): added c-select in the ui lib

* refactor(ui): switched n-select to c-select

* feat(new tool): emoji picker (#551)

* chore(deps): update dependency @vitejs/plugin-vue to v4 (#496)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency @vitejs/plugin-vue-jsx to v3 (#497)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* refactor(case converter): using nocase to convert to upper and lower case

* refactor(case converter): config based case changes

---------

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-09 23:45:21 +02:00
Corentin THOMASSET
56d74d07a8 fix(c-lib): hide component library shortcut link in non-dev (#557) 2023-08-09 20:58:15 +00:00
Corentin THOMASSET
e5d0ba7073 fix(emoji picker): fix copy button (#556) 2023-08-09 20:54:49 +00:00
renovate[bot]
1a602365be chore(deps): update dependency @vitejs/plugin-vue-jsx to v3 (#497)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 22:25:59 +02:00
renovate[bot]
a2494219a8 chore(deps): update dependency @vitejs/plugin-vue to v4 (#496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 22:25:59 +02:00
Corentin THOMASSET
93f7cf0e98 feat(new tool): emoji picker (#551) 2023-08-08 22:25:59 +02:00
Corentin THOMASSET
dfa1ba8554 feat(ui): added c-select in the ui lib (#550)
* feat(ui): added c-select in the ui lib

* refactor(ui): switched n-select to c-select
2023-08-07 15:30:00 +00:00
renovate[bot]
6498c9b0fa chore(deps): update dependency vite-plugin-pwa to ^0.16.0 (#488)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-05 16:29:58 +02:00
renovate[bot]
f40d7ecddf chore(deps): update dependency vite to v4 (#503)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-29 20:35:23 +02:00
Corentin THOMASSET
2e28c5073e chore(ci): e2e against vercel deployement (#518) 2023-06-29 20:27:35 +02:00
renovate[bot]
d12dd40841 fix(deps): update dependency @vueuse/head to v1 (#515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-29 20:20:28 +02:00
Corentin THOMASSET
cf382b5a10 chore(e2e): execute e2e against built app (#511) 2023-06-29 18:48:51 +02:00
renovate[bot]
01525838e0 chore(deps): update github/codeql-action action to v2 (#513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-29 08:57:10 +02:00
renovate[bot]
38cb61da4d chore(deps): update node.js to v18 (#514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-28 08:35:45 +02:00
Corentin THOMASSET
354aed6e6f chore(deps): switched from vite-plugin-md to vite-plugin-vue-markdown (#510) 2023-06-27 18:39:20 +00:00
renovate[bot]
6b8682fd23 chore(deps): update dependency workbox-window to v7 (#509)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 20:29:41 +02:00
renovate[bot]
9e8349dbc4 chore(deps): update dependency vite-svg-loader to v4 (#508)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 20:13:06 +02:00
renovate[bot]
f44050742d chore(deps): update dependency typescript to ~4.9.0 (#481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 20:08:12 +02:00
renovate[bot]
b0d9a3e6c7 chore(deps): update dependency vue-tsc to ^0.40.0 (#490)
* chore(deps): update dependency vue-tsc to ^0.40.0

* chore(deps): upgraded vue-tsc dependency

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2023-06-27 17:59:05 +00:00
Corentin THOMASSET
30f88fc6a8 refactor(navbar): consistent spacing in navbar buttons (#507) 2023-06-27 16:47:32 +00:00
Corentin THOMASSET
72c98a3c5e refactor(ui): remove n-text (#506) 2023-06-25 13:49:43 +00:00
Corentin THOMASSET
05ea545475 refactor(ui): replaced some n-input to c-input (#505) 2023-06-25 13:00:50 +00:00
Corentin THOMASSET
5c3bebfe62 chore(deps): updated unplugin-auto-import (#504) 2023-06-25 09:45:56 +00:00
renovate[bot]
8df7cd0f19 chore(deps): removed start-server-and-test dependency
* chore(deps): update dependency start-server-and-test to v2

* chore(deps): removed start-server-and-test dependency

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2023-06-25 08:34:36 +00:00
Corentin THOMASSET
a9c7b89193 feat(new-tool): password strength analyzer (#502) 2023-06-25 08:26:29 +00:00
renovate[bot]
6bda2caa04 chore(deps): update dependency c8 to v8 (#498)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-25 10:02:46 +02:00
renovate[bot]
994a1c3401 chore(deps): update dependency @types/jsdom to v21 (#495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-24 18:55:46 +02:00
renovate[bot]
05edaf423c chore(deps): update node.js to v16.20.1 (#491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-24 18:54:36 +02:00
renovate[bot]
8c72e692a7 fix(deps): update dependency country-code-lookup to ^0.1.0 (#493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-24 18:49:54 +02:00
renovate[bot]
cec9dea9e0 fix(deps): update dependency @vueuse/head to ^0.9.0 (#492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-24 18:48:42 +02:00
renovate[bot]
49eacea195 chore(deps): update dependency vitest to ^0.32.0 (#489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-24 18:47:57 +02:00
renovate[bot]
3f7d469e9f chore(deps): update actions/checkout action to v3 (#494)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-24 18:46:01 +02:00
renovate[bot]
5f2190887d chore(deps): update dependency unplugin-vue-components to ^0.25.0 (#484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-23 22:19:08 +02:00
renovate[bot]
6cb0845336 chore(deps): update dependency unplugin-auto-import to ^0.16.0 (#483)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-23 22:18:44 +02:00
Corentin Thomasset
12d9e5d377 fix(i18n): fallback for demo i18n 2023-06-23 21:57:08 +02:00
Corentin Thomasset
e29b258e90 feat(new-tool): yaml to toml 2023-06-23 21:47:23 +02:00
Corentin Thomasset
ea50a3fc65 feat(new-tool): json to toml 2023-06-23 21:47:23 +02:00
Corentin Thomasset
746e5bdccc feat(new-tool): toml to yaml 2023-06-23 21:47:23 +02:00
Corentin Thomasset
c7d4f112c0 feat(new-tool): toml to json 2023-06-23 21:47:23 +02:00
Marvin
9125dcf9c6 refactor(json-viewer): input monospace font (#485) 2023-06-23 19:38:34 +00:00
renovate[bot]
38710dce56 chore(deps): update dependency unocss to ^0.53.0 (#482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-23 21:30:45 +02:00
renovate[bot]
282cfc4c4b chore(deps): update dependency @unocss/eslint-config to ^0.53.0 (#478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-23 16:59:18 +02:00
Marvin
ba4876d0d5 refacor(transformers): use monospace font for JSON and SQL text areas (#476)
* feat(c-input): Add monospace prop

* feat: use monospace input for json and sql inputs
2023-06-23 16:51:52 +02:00
renovate[bot]
363c2e47e6 chore(deps): added renovate.json (#477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-23 16:49:03 +02:00
Marvin
9526ed8324 fix(typos): fixed more typos & uppercase JSON (#475)
* fix(About): typo

* uppercase json & typos
2023-06-23 15:23:47 +02:00
Marvin
7068610438 fix(about): typos and wording (#474)
* fix(About): typos and wording

* Update src/pages/About.vue

---------

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
2023-06-21 19:45:48 +00:00
Corentin THOMASSET
847323ccba docs(ide): updated vscode extensions settings (#472) 2023-06-19 21:49:40 +00:00
Corentin THOMASSET
1b038c7826 chore(i18n): tool scoped locales (#471) 2023-06-19 21:14:44 +00:00
Corentin Thomasset
ec4c533718 feat(command-palette): random tool action 2023-06-19 21:51:25 +02:00
Seb
63045951e1 feat(config): allow app to run in a subfolder via BASE_URL (#461)
* fix: allow hosting in subfolder via BASE_URL

* Import baseUrl from config

* Revert "Import baseUrl from config"

This reverts commit 73c867ebc8.

* feat(config): fallback baseUrl to /

* feat(config): fallback baseUrl to /

---------

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2023-06-19 18:42:29 +02:00
Corentin THOMASSET
c4cec9e18f fix(mime-types): typos (#470) 2023-06-19 16:09:03 +00:00
Corentin THOMASSET
bcb98b359c refactor(search): command palette design (#463) 2023-06-19 00:35:50 +02:00
Corentin Thomasset
732da08157 chore(wysiwyg-editor): update tiptap dependencies 2023-06-19 00:08:07 +02:00
Seb
b9406a492d feat(new-tool): percentage calculator (#456)
* feat(new tool): percentage calculator

* Apply suggestions from code review

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

---------

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
2023-06-18 18:24:39 +02:00
Corentin Thomasset
69f0bd079f feat(new-tool): json to csv converter 2023-06-18 18:10:06 +02:00
Corentin Thomasset
4cbd7ac145 fix(sonar): took down minor sonar warning 2023-06-18 14:56:52 +02:00
Corentin Thomasset
ebfb872fae chore(i18n): setup i18n plugin config 2023-06-18 14:33:58 +02:00
jmmanzano
a6bbeaebd8 feat(new tool): xml formatter (#457)
* feat(new tool): xml formatter

* feat(xml-formatter): added happy path e2e tests

* refactor(xml-formatter): improved unit tests

* refactor(xml-formatter): add better suitable icon

* feat(xml-formatter): added happy path e2e tests

* feat(xml-formatter): registered xml as syntax highlighter

* chore(auto-import): removed unused NSpace

---------

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
2023-06-18 10:27:26 +00:00
myztillx
f771e7a99f feat(chmod-calculator): added symbolic representation (#455) 2023-06-18 11:01:54 +02:00
Seb
cf7b1f000a feat(enhancement): use system dark mode (#458)
* Use prefers-color-scheme

* Remove theme store
2023-06-18 10:59:22 +02:00
Corentin Thomasset
1e2a35b892 refactor(c-input-text): force usage of props with default 2023-06-11 21:27:40 +02:00
Corentin Thomasset
45c2474279 refactor(naming): prevent auto import conflicts for git memo 2023-06-10 17:37:38 +02:00
Corentin Thomasset
fe61f0f2f2 refactor(imports): removed unnecessary imports to vue 2023-06-10 17:24:10 +02:00
David Nguyen
93799af83c chore(config): netlify deployment support (#443)
* Create netlify.toml

* Update netlify.toml

---------

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
2023-06-10 17:02:08 +02:00
Corentin Thomasset
962a6d6ec4 chore(ci): shard e2e tests 2023-06-10 16:59:54 +02:00
Corentin Thomasset
d2956b66fe feat(phone-parser): searchable country code select 2023-06-10 16:43:03 +02:00
DawnElixir
105b21badc fix(readme): typo
Fix readme typo
2023-05-31 22:55:44 +02:00
Corentin Thomasset
33c9b6643f chore(lint): switched to a better lint config 2023-05-28 23:29:14 +02:00
Corentin Thomasset
4d2b037dbe refactor(ui): removed all n-space 2023-05-27 17:53:13 +02:00
Corentin Thomasset
34d8e5ce2c feat(new tool): camera screenshot and recorder 2023-05-17 00:34:10 +02:00
Corentin Thomasset
8515c24264 chors(deps): updated vueuse dependency version 2023-05-15 17:21:59 +02:00
cgoIT
0b20f1c16a feat(base64-string-converter): switch to encode and decode url safe base64 strings (#392)
* feat(base64-string-converter): switch to encode and decode url safe

* feat(base64-string-converter): changes based on review comments, use config object instead of boolean argument.

* feat(base64-string-converter): fix validation, add option to watch additional refs for changes which interfere with validation rules
2023-05-15 14:35:44 +02:00
cgoIT
8c92d56318 fix(ipv4-range-expander): calculate correct for ip addresses where the first octet is lower than 128 (#405) 2023-05-15 10:23:16 +02:00
Corentin Thomasset
7aed9c56fd fix(ipv4-converter): removed readonly on input 2023-05-15 10:00:55 +02:00
Corentin Thomasset
f7fc779e63 refactor(ui): replaced some n-input with c-input-text 2023-05-15 01:31:22 +02:00
Corentin Thomasset
b3b6b7c46b chore(version): release 2023.05.14-77f2efc 2023-05-14 22:48:20 +02:00
Corentin Thomasset
141c12455e docs(changelog): update changelog for 2023.05.14-77f2efc 2023-05-14 22:48:20 +02:00
Corentin Thomasset
77f2efc0b9 refactor(ui): replaced some n-input with c-input-text 2023-05-14 22:30:23 +02:00
Corentin Thomasset
aad8d84e13 ui-lib(new-component): added text input component in the c-lib 2023-05-14 22:30:23 +02:00
Corentin Thomasset
401f13f7e3 ui-lib(button): size variants 2023-05-14 22:30:23 +02:00
Corentin THOMASSET
edae4c6915 chore(issues): updated new tool request issue template 2023-05-13 22:09:55 +02:00
Corentin Thomasset
a43c546e34 fix(phone-parser): use default country code 2023-05-07 13:25:33 +02:00
cgoIT
83a7b3bae9 feat(list-converter): a small converter who deals with column based data and do some stuff with it (#387)
* feat(list-converter): a small converter who deals with column based data and do some stuff with it

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

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

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

---------

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

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

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

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

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

* feat(ipv4-range-expander): only show n-alert if both ipv4 addresses are valid
2023-04-19 20:30:45 +02:00
Corentin Thomasset
6d2202597c feat(date converter): auto focus main input 2023-04-19 13:07:24 +02:00
353 changed files with 16985 additions and 7277 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
node_modules
playwright-report
coverage
dist
test-results

View File

@@ -22,7 +22,9 @@
"createGlobalState": true,
"createInjectionState": true,
"createReactiveFn": true,
"createReusableTemplate": true,
"createSharedComposable": true,
"createTemplatePromise": true,
"createUnrefFn": true,
"customRef": true,
"debouncedRef": true,
@@ -42,9 +44,6 @@
"isReactive": true,
"isReadonly": true,
"isRef": true,
"logicAnd": true,
"logicNot": true,
"logicOr": true,
"makeDestructurable": true,
"markRaw": true,
"nextTick": true,
@@ -107,6 +106,19 @@
"unrefElement": true,
"until": true,
"useActiveElement": true,
"useAnimate": true,
"useArrayDifference": true,
"useArrayEvery": true,
"useArrayFilter": true,
"useArrayFind": true,
"useArrayFindIndex": true,
"useArrayFindLast": true,
"useArrayIncludes": true,
"useArrayJoin": true,
"useArrayMap": true,
"useArrayReduce": true,
"useArraySome": true,
"useArrayUnique": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
@@ -117,8 +129,8 @@
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useCached": true,
"useClamp": true,
"useClipboard": true,
"useCloned": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCounter": true,
@@ -160,6 +172,7 @@
"useFullscreen": true,
"useGamepad": true,
"useGeolocation": true,
"useI18n": true,
"useIdle": true,
"useImage": true,
"useInfiniteScroll": true,
@@ -192,12 +205,18 @@
"useOnline": true,
"usePageLeave": true,
"useParallax": true,
"useParentElement": true,
"usePerformanceObserver": true,
"usePermission": true,
"usePointer": true,
"usePointerLock": true,
"usePointerSwipe": true,
"usePreferredColorScheme": true,
"usePreferredContrast": true,
"usePreferredDark": true,
"usePreferredLanguages": true,
"usePreferredReducedMotion": true,
"usePrevious": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
@@ -211,14 +230,17 @@
"useSessionStorage": true,
"useShare": true,
"useSlots": true,
"useSorted": true,
"useSpeechRecognition": true,
"useSpeechSynthesis": true,
"useStepper": true,
"useStorage": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSupported": true,
"useSwipe": true,
"useTemplateRefsList": true,
"useTextDirection": true,
"useTextSelection": true,
"useTextareaAutosize": true,
"useThrottle": true,
@@ -230,6 +252,8 @@
"useTimeoutPoll": true,
"useTimestamp": true,
"useTitle": true,
"useToNumber": true,
"useToString": true,
"useToggle": true,
"useTransition": true,
"useUrlSearchParams": true,
@@ -250,8 +274,10 @@
"watchArray": true,
"watchAtMost": true,
"watchDebounced": true,
"watchDeep": true,
"watchEffect": true,
"watchIgnorable": true,
"watchImmediate": true,
"watchOnce": true,
"watchPausable": true,
"watchPostEffect": true,
@@ -259,6 +285,7 @@
"watchThrottled": true,
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true
"whenever": true,
"toValue": true
}
}
}

View File

@@ -1,39 +1,21 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');
/**
* @type {import('eslint').Linter.Config}
*/
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier',
'plugin:import/recommended',
'./.eslintrc-auto-import.json',
'@unocss',
],
extends: ['@antfu', './.eslintrc-auto-import.json', '@unocss'],
settings: {
'import/resolver': { typescript: { project: './tsconfig.app.json' } },
},
env: {
'vue/setup-compiler-macros': true,
},
rules: {
'vue/multi-word-component-names': ['off'],
'prettier/prettier': ['error'],
'import/no-duplicates': ['error', { considerQueryString: true }],
'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }],
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-unresolved': ['error', { ignore: ['^virtual:'] }],
'curly': ['error', 'all'],
'@typescript-eslint/semi': ['error', 'always'],
'@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
'vue/no-empty-component-block': ['error'],
'no-restricted-imports': ['error', {
paths: [{
name: '@vueuse/core',
importNames: ['useClipboard'],
message: 'Please use local useCopy from src/composable/copy.ts instead of useClipboard.',
}],
}],
},
};

View File

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

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- run: corepack enable
- uses: actions/setup-node@v3
with:
@@ -27,5 +27,8 @@ jobs:
- name: Run unit test
run: pnpm test
- name: Type check
run: pnpm typecheck
- name: Build the app
run: pnpm build

View File

@@ -37,11 +37,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -52,7 +52,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -66,4 +66,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@@ -12,7 +12,7 @@ jobs:
outputs:
should_run: ${{ steps.should_run.outputs.should_run }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: print latest_commit
run: echo ${{ github.sha }}
@@ -28,7 +28,7 @@ jobs:
if: ${{ needs.check_date.outputs.should_run != 'false' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- run: corepack enable
- uses: actions/setup-node@v3
with:
@@ -54,29 +54,29 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile

47
.github/workflows/e2e-tests.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: E2E tests
on:
pull_request:
push:
branches:
- main
jobs:
test:
timeout-minutes: 10
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1/3, 2/3, 3/3]
steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- run: corepack enable
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'pnpm'
- name: Get Playwright version
id: playwright-version
run: echo "PLAYWRIGHT_VERSION=$(jq -r .dependencies.playwright package.json)" >> "$GITHUB_OUTPUT"
- name: Install dependencies
run: pnpm install
- name: Build app
run: pnpm build
- name: Restore Playwright browsers from cache
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-${{ hashFiles('**/playwright.config.ts') }}
restore-keys: |
${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-
${{ runner.os }}-playwright-
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm run test:e2e --shard=${{ matrix.shard }}

View File

@@ -1,23 +0,0 @@
name: E2E tests
on:
pull_request:
push:
branches:
- main
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: corepack enable
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm exec playwright test

View File

@@ -13,29 +13,29 @@ jobs:
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
@@ -55,7 +55,7 @@ jobs:
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
- run: corepack enable

2
.gitignore vendored
View File

@@ -31,3 +31,5 @@ coverage
/test-results/
/playwright-report/
/playwright/.cache/
# Webkit with playwright creates a salt file
salt

2
.nvmrc
View File

@@ -1 +1 @@
16.14.2
18.18.2

View File

@@ -1,3 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin", "dbaeumer.vscode-eslint"]
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin", "dbaeumer.vscode-eslint", "lokalise.i18n-ally"]
}

View File

@@ -2,6 +2,266 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## Version 2023.12.21-5ed3693
### Features
- **i18n**: improve chinese i18n (#757) (2e56641)
- **i18n**: add tooltip and favoriteButton i18n (#756) (a1037cf)
- **i18n**: add Chinese translation base (#718) (8f99eb6)
- **new tool**: pdf signature checker (#745) (4781920)
- **new tool**: numeronym generator (#729) (e07e2ae)
### Bug fixes
- **jwt-parser**: jwt claim array support (#799) (5ed3693)
- **camera-recorder**: stop camera on navigation (#782) (80e46c9)
- **doc**: updated create new tool command in readme (#762) (7a70dbb)
- **base64-file-converter**: fix downloading of index.html content without data preambula (#750) (043e4f0)
- **docker**: rollback armv7 in docker releases (#741) (205e360)
- **eta**: corrected example (#737) (821cbea)
### Refactoring
- **about, i18n**: improved i18n dx with markdown (#753) (bd3edcb)
- **token, i18n**: complete fr translation (#752) (de1ee69)
- **uuid generator**: uuid version picker (#751) (38586ca)
- **case converter**: no split on lowercase, uppercase and mocking case (#748) (ca43a25)
- **ui**: replaced legacy n-upload with c-file-upload (#747) (7fe47b3)
- **token**: added password in token generator keywords (#746) (16ffe6b)
- **bcrypt**: fix input label align (#721) (093ff31)
### Chores
- **deps**: switched from oui to oui-data for mac address lookup (#693) (0fe9a20)
- **deps**: update unocss monorepo to ^0.57.0 (#638) (2e396d8)
- **docker**: added armv7 plateform for docker releases (#722) (fe1de8c)
## Version 2023.11.02-7d94e11
### Features
- **i18n**: language selector (#710) (e86fd96)
### Bug fixes
- **dockerfile**: revert replacement of nginx image with non-privileged one (#716) (7d94e11)
- **encryption**: alert on decryption error (#711) (02b0d0d)
### Refactoring
- **math-evaluator**: improved description (e87f4b1)
- **math-evaluator**: improved search and UX (#713) (58de897)
## Version 2023.11.01-e164afb
### Features
- **command-palette**: clear prompt on palette close (#708) (d013696)
- **command-palette**: added about page in command palette (99b1eb9)
- **new tool**: random MAC address generator (#657) (cc3425d)
- **case-converter**: added mocking case (#705) (681f7bf)
- **date-converter**: added excel date time format (#704) (f5eb7a8)
- **i18n**: token generator (#688) (02e68d3)
- **i18n**: home page (#687) (00562ed)
- **i18n**: support for i18n in .ts files (#683) (ebb4ec4)
- **i18n**: tool card (#682) (84a4a64)
- **i18n**: about page (#680) (a2b53c2)
- **i18n**: 404 page (#679) (35563b8)
- **new tool**: text to ascii converter (#669) (b2ad4f7)
- **new tool**: ULID generator (#623) (5c4d775)
- **new tool**: add wifi qr code generator (#599) (0eedce6)
- **new tool**: iban validation and parser (#591) (3a63837)
- **new tool**: text diff and comparator (#588) (81bfe57)
### Bug fixes
- **deps**: fix issue on slugify (#593) (#673) (720201a)
- **deps**: update dependency monaco-editor to ^0.43.0 (#620) (e371ef7)
- **deps**: update dependency sql-formatter to v13 (#606) (c7d4562)
### Refactoring
- **ui**: better ui demo preview menu (#664) (015c673)
- **color-converter**: improved color-converter UX (#701) (abb8335)
- **docker**: improved docker config (#700) (020e9cb)
- **c-table**: added description on c-table for accessibility (b408df8)
- **ci**: reduced timeout in e2e (#666) (88b8818)
- **ui**: new c-table ui component (#665) (ee4c853)
- **ui**: removed n-page-header component in user-agent parser (#663) (cbf58fd)
- **ui**: removed n-p components in about page (#662) (a757a51)
- **ui**: switched naive tooltip components to custom ones (#661) (025f556)
- **spelling**: minor corrections to phrasing/spelling (#596) (8a30b6b)
- **i18n**: merge tools scoped locales with global ones (#612) (233d556)
- **c-key-value-list**: got rid of table for layout (#611) (7ab9204)
- **CI**: run e2e against built app and no longer vercel (#610) (18dd140)
- **bcrypt**: fix typo (#604) (e18bae1)
### Chores
- **deps**: clean unused dependencies (#709) (e164afb)
- **deps**: update docker/setup-qemu-action action to v3 (#627) (4365226)
- **deps**: update docker/setup-buildx-action action to v3 (#626) (57ecda1)
- **deps**: update docker/login-action action to v3 (#625) (d8d7a3b)
- **deps**: update docker/build-push-action action to v5 (#624) (d36b18f)
- **deps**: update dependency node to v18.18.2 (#674) (eea9f91)
- **deps**: update dependency node to v18.18.0 (#636) (2d2dffb)
- **deps**: update actions/checkout action to v4 (#613) (4972159)
- **deps**: update dependency unplugin-icons to ^0.17.0 (#609) (f035f48)
- **deps**: update dependency @intlify/unplugin-vue-i18n to ^0.13.0 (#597) (d1dff42)
- **deps**: update dependency @antfu/eslint-config to ^0.41.0 (#585) (a9cd91c)
- **deps**: update dependency typescript to ~5.2.0 (#587) (f3e14fc)
### Doc
- **readme**: added contributors list (#622) (557b304)
- **hosting**: added cloudron in the other hosting solutions section (#589) (06c3547)
## Version 2023.08.21-6f93cba
### Features
- **copy**: support legacy copy to clipboard for older browser (#581) (6f93cba)
- **new tool**: string obfuscator (#575) (c58d6e3)
### Bug fixes
- **deps**: update dependency sql-formatter to v12 (#520) (2bcb77a)
### Chores
- **deps**: switched to fucking typescript v5 (#501) (76b2761)
- **deps**: update dependency @antfu/eslint-config to ^0.40.0 (#552) (6ff9a01)
- **deps**: update dependency prettier to v3 (#564) (a2b9b15)
- **deps**: removed @typescript-eslint/parser (#563) (144f86e)
- **deps**: removed ts-pattern (#565) (0f1f659)
## Version 2023.08.16-9bd4ad4
### Features
- **Case Converter**: Add lowercase and uppercase (#534) (7b6232a)
- **new tool**: emoji picker (#551) (93f7cf0)
- **ui**: added c-select in the ui lib (#550) (dfa1ba8)
- **new-tool**: password strength analyzer (#502) (a9c7b89)
- **new-tool**: yaml to toml (e29b258)
- **new-tool**: json to toml (ea50a3f)
- **new-tool**: toml to yaml (746e5bd)
- **new-tool**: toml to json (c7d4f11)
- **command-palette**: random tool action (ec4c533)
- **config**: allow app to run in a subfolder via BASE_URL (#461) (6304595)
- **new-tool**: percentage calculator (#456) (b9406a4)
- **new-tool**: json to csv converter (69f0bd0)
- **new tool**: xml formatter (#457) (a6bbeae)
- **chmod-calculator**: added symbolic representation (#455) (f771e7a)
- **enhancement**: use system dark mode (#458) (cf7b1f0)
- **phone-parser**: searchable country code select (d2956b6)
- **new tool**: camera screenshot and recorder (34d8e5c)
- **base64-string-converter**: switch to encode and decode url safe base64 strings (#392) (0b20f1c)
### Bug fixes
- **deps**: update dependency uuid to v9 (#566) (5e12991)
- **deps**: update dependency mathjs to v11 (#519) (7924456)
- **deps**: update dependency @vueuse/router to v10 (#516) (ea0f27c)
- **copy**: prevent shorthand copy if source is present in useCopy (#559) (86e964a)
- **c-lib**: hide component library shortcut link in non-dev (#557) (56d74d0)
- **emoji picker**: fix copy button (#556) (e5d0ba7)
- **deps**: update dependency @vueuse/head to v1 (#515) (d12dd40)
- **deps**: update dependency country-code-lookup to ^0.1.0 (#493) (8c72e69)
- **deps**: update dependency @vueuse/head to ^0.9.0 (#492) (cec9dea)
- **i18n**: fallback for demo i18n (12d9e5d)
- **typos**: fixed more typos & uppercase JSON (#475) (9526ed8)
- **about**: typos and wording (#474) (7068610)
- **mime-types**: typos (#470) (c4cec9e)
- **sonar**: took down minor sonar warning (4cbd7ac)
- **readme**: typo (105b21b)
- **ipv4-range-expander**: calculate correct for ip addresses where the first octet is lower than 128 (#405) (8c92d56)
- **ipv4-converter**: removed readonly on input (7aed9c5)
### Refactoring
- **navbar**: consistent spacing in navbar buttons (#507) (30f88fc)
- **ui**: remove n-text (#506) (72c98a3)
- **ui**: replaced some n-input to c-input (#505) (05ea545)
- **json-viewer**: input monospace font (#485) (9125dcf)
- **search**: command palette design (#463) (bcb98b3)
- **c-input-text**: force usage of props with default (1e2a35b)
- **naming**: prevent auto import conflicts for git memo (45c2474)
- **imports**: removed unnecessary imports to vue (fe61f0f)
- **ui**: removed all n-space (4d2b037)
- **ui**: replaced some n-input with c-input-text (f7fc779)
### Chores
- **deps**: update dependency vitest to ^0.34.0 (#562) (9bd4ad4)
- **deps**: update dependency node to v18.17.1 (#560) (65a9474)
- **deps**: update dependency unocss to ^0.55.0 (#561) (85cc7a8)
- **deps**: update dependency @unocss/eslint-config to ^0.55.0 (#553) (4268e25)
- **deps**: update dependency @intlify/unplugin-vue-i18n to ^0.12.0 (#526) (d1c8880)
- **deps**: update docker/login-action action to v2 (#512) (99bc84c)
- **deps**: update dependency jsdom to v22 (#499) (cd5a503)
- **deps**: update dependency @vitejs/plugin-vue-jsx to v3 (#497) (1a60236)
- **deps**: update dependency @vitejs/plugin-vue to v4 (#496) (a249421)
- **deps**: update dependency vite-plugin-pwa to ^0.16.0 (#488) (6498c9b)
- **deps**: update dependency vite to v4 (#503) (f40d7ec)
- **ci**: e2e against vercel deployement (#518) (2e28c50)
- **e2e**: execute e2e against built app (#511) (cf382b5)
- **deps**: update github/codeql-action action to v2 (#513) (0152583)
- **deps**: update node.js to v18 (#514) (38cb61d)
- **deps**: switched from vite-plugin-md to vite-plugin-vue-markdown (#510) (354aed6)
- **deps**: update dependency workbox-window to v7 (#509) (6b8682f)
- **deps**: update dependency vite-svg-loader to v4 (#508) (9e8349d)
- **deps**: update dependency typescript to ~4.9.0 (#481) (f440507)
- **deps**: update dependency vue-tsc to ^0.40.0 (#490) (b0d9a3e)
- **deps**: updated unplugin-auto-import (#504) (5c3bebf)
- **deps**: removed start-server-and-test dependency (8df7cd0)
- **deps**: update dependency c8 to v8 (#498) (6bda2ca)
- **deps**: update dependency @types/jsdom to v21 (#495) (994a1c3)
- **deps**: update node.js to v16.20.1 (#491) (05edaf4)
- **deps**: update dependency vitest to ^0.32.0 (#489) (49eacea)
- **deps**: update actions/checkout action to v3 (#494) (3f7d469)
- **deps**: update dependency unplugin-vue-components to ^0.25.0 (#484) (5f21908)
- **deps**: update dependency unplugin-auto-import to ^0.16.0 (#483) (6cb0845)
- **deps**: update dependency unocss to ^0.53.0 (#482) (38710dc)
- **deps**: update dependency @unocss/eslint-config to ^0.53.0 (#478) (282cfc4)
- **deps**: added renovate.json (#477) (363c2e4)
- **i18n**: tool scoped locales (#471) (1b038c7)
- **wysiwyg-editor**: update tiptap dependencies (732da08)
- **i18n**: setup i18n plugin config (ebfb872)
- **config**: netlify deployment support (#443) (93799af)
- **ci**: shard e2e tests (962a6d6)
- **lint**: switched to a better lint config (33c9b66)
### Refacor
- **transformers**: use monospace font for JSON and SQL text areas (#476) (ba4876d)
### Documentation
- **ide**: updated vscode extensions settings (#472) (847323c)
### Chors
- **deps**: updated vueuse dependency version (8515c24)
## Version 2023.05.14-77f2efc
### Features
- **list-converter**: a small converter who deals with column based data and do some stuff with it (#387) (83a7b3b)
- **new tool**: phone parser and normalizer (ce3150c)
### Bug fixes
- **phone-parser**: use default country code (a43c546)
- **home**: prevent weird blue border on card (3f6c8f0)
### Refactoring
- **ui**: replaced some n-input with c-input-text (77f2efc)
### Chores
- **issues**: updated new tool request issue template (edae4c6)
### Ui-lib
- **new-component**: added text input component in the c-lib (aad8d84)
- **button**: size variants (401f13f)
## Version 2023.04.23-92bd835
### Features
- **ui-lib**: demo pages for c-lib components (92bd835)
- **new-tool**: diff of two json objects (362f2fa)
- **ipv4-range-expander**: expands a given IPv4 start and end address to a valid IPv4 subnet (#366) (df989e2)
- **date converter**: auto focus main input (6d22025)
### Bug fixes
- **ts**: cleaned legacy typechecking warning (e88c1d5)
- **mac-address-lookup**: added copy handler on button click (c311e38)
### Refactoring
- **ui-lib**: prevent c-button to shrink (61ece23)
- **ui**: replaced naive ui cards with custom ones (f080933)
- **clean**: removed unused lodash import (bb32513)
- **clean**: removed useless br tags (74073f5)
- **ui**: getting ride of naive ui buttons (c45bce3)
## Version 2023.04.14-dbad773
### Features

View File

@@ -1,9 +1,12 @@
# build stage
FROM node:lts-alpine AS build-stage
# Set environment variables for non-interactive npm installs
ENV NPM_CONFIG_LOGLEVEL warn
ENV CI true
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm i --frozen-lockfile
COPY . .
RUN npm install -g pnpm
RUN pnpm i --frozen-lockfile
RUN pnpm build
# production stage
@@ -11,4 +14,4 @@ FROM nginx:stable-alpine AS production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -6,7 +6,7 @@ Useful tools for developer and people working in IT. [Have a look !](https://it-
Please check the [issues](https://github.com/CorentinTh/it-tools/issues) to see if some feature listed to be implemented.
You have an idea of a tool? Submit a [feature request](https://github.com/CorentinTh/it-tools/issues/new?assignees=corentinth&labels=&template=feature_request.md&title=)!
You have an idea of a tool? Submit a [feature request](https://github.com/CorentinTh/it-tools/issues/new/choose)!
## Self host
@@ -26,6 +26,7 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
**Other solutions:**
- [Cloudron](https://www.cloudron.io/store/tech.ittools.cloudron.html)
- [Tipi](https://www.runtipi.io/docs/apps-available)
- [Unraid](https://unraid.net/community/apps?q=it-tools)
@@ -33,7 +34,25 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
### Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
[VSCode](https://code.visualstudio.com/) with the following extensions:
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur)
- [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally)
with the following settings:
```json
{
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"i18n-ally.localesPaths": ["locales", "src/tools/*/locales"],
"i18n-ally.keystyle": "nested"
}
```
### Type Support for `.vue` Imports in TS
@@ -81,10 +100,16 @@ pnpm lint
To create a new tool, there is a script that generate the boilerplate of the new tool, simply run:
```sh
pnpm run script:create-new-tool my-tool-name
pnpm run script:create:tool my-tool-name
```
It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the inported tool in the proper category and develop the tool.
It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the imported tool in the proper category and develop the tool.
## Contributors
Big thanks to all the people who have already contributed!
[![contributors](https://contrib.rocks/image?repo=corentinth/it-tools)](https://github.com/corentinth/it-tools/graphs/contributors)
## Credits
@@ -92,6 +117,8 @@ Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr).
This project is continuously deployed using [vercel.com](https://vercel.com).
Contributor graph is generated using [contrib.rocks](https://contrib.rocks/preview?repo=corentinth/it-tools).
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-it&#0045;tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT&#0032;Tools - Collection&#0032;of&#0032;handy&#0032;online&#0032;tools&#0032;for&#0032;devs&#0044;&#0032;with&#0032;great&#0032;UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-it&#0045;tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=345793&theme=light&period=daily" alt="IT&#0032;Tools - Collection&#0032;of&#0032;handy&#0032;online&#0032;tools&#0032;for&#0032;devs&#0044;&#0032;with&#0032;great&#0032;UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>

View File

@@ -0,0 +1,6 @@
---
to: src/ui/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.demo.vue
---
<template>
<<%= h.changeCase.param(name) %> />
</template>

View File

@@ -0,0 +1,13 @@
---
to: src/ui/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.vue
---
<script lang="ts" setup>
const props = withDefaults(defineProps<{ prop?: string }>(), { prop: '' });
const { prop } = toRefs(props);
</script>
<template>
<div>
{{ prop }}
</div>
</template>

354
auto-imports.d.ts vendored
View File

@@ -19,7 +19,9 @@ declare global {
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
@@ -39,9 +41,6 @@ declare global {
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const logicAnd: typeof import('@vueuse/core')['logicAnd']
const logicNot: typeof import('@vueuse/core')['logicNot']
const logicOr: typeof import('@vueuse/core')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
@@ -94,6 +93,7 @@ declare global {
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
@@ -104,6 +104,19 @@ declare global {
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
@@ -114,8 +127,8 @@ declare global {
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClamp: typeof import('@vueuse/core')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
@@ -157,6 +170,7 @@ declare global {
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useI18n: typeof import('vue-i18n')['useI18n']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
@@ -189,12 +203,18 @@ declare global {
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
@@ -208,14 +228,17 @@ declare global {
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
@@ -227,6 +250,8 @@ declare global {
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
@@ -247,8 +272,10 @@ declare global {
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
@@ -282,7 +309,9 @@ declare module 'vue' {
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
@@ -302,9 +331,6 @@ declare module 'vue' {
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly logicAnd: UnwrapRef<typeof import('@vueuse/core')['logicAnd']>
readonly logicNot: UnwrapRef<typeof import('@vueuse/core')['logicNot']>
readonly logicOr: UnwrapRef<typeof import('@vueuse/core')['logicOr']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
@@ -357,6 +383,7 @@ declare module 'vue' {
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@@ -367,6 +394,19 @@ declare module 'vue' {
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
@@ -377,8 +417,8 @@ declare module 'vue' {
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClamp: UnwrapRef<typeof import('@vueuse/core')['useClamp']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
@@ -420,6 +460,7 @@ declare module 'vue' {
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
@@ -452,12 +493,18 @@ declare module 'vue' {
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
@@ -471,14 +518,17 @@ declare module 'vue' {
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
@@ -490,6 +540,8 @@ declare module 'vue' {
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
@@ -510,8 +562,294 @@ declare module 'vue' {
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDialog: UnwrapRef<typeof import('naive-ui')['useDialog']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNotification: UnwrapRef<typeof import('naive-ui')['useNotification']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>

172
components.d.ts vendored
View File

@@ -9,18 +9,136 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
'404.page': typeof import('./src/pages/404.page.vue')['default']
About: typeof import('./src/pages/About.vue')['default']
App: typeof import('./src/App.vue')['default']
'Base.layout': typeof import('./src/layouts/base.layout.vue')['default']
Base64FileConverter: typeof import('./src/tools/base64-file-converter/base64-file-converter.vue')['default']
Base64StringConverter: typeof import('./src/tools/base64-string-converter/base64-string-converter.vue')['default']
BasicAuthGenerator: typeof import('./src/tools/basic-auth-generator/basic-auth-generator.vue')['default']
Bcrypt: typeof import('./src/tools/bcrypt/bcrypt.vue')['default']
BenchmarkBuilder: typeof import('./src/tools/benchmark-builder/benchmark-builder.vue')['default']
Bip39Generator: typeof import('./src/tools/bip39-generator/bip39-generator.vue')['default']
CAlert: typeof import('./src/ui/c-alert/c-alert.vue')['default']
'CAlert.demo': typeof import('./src/ui/c-alert/c-alert.demo.vue')['default']
CameraRecorder: typeof import('./src/tools/camera-recorder/camera-recorder.vue')['default']
CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default']
CButton: typeof import('./src/ui/c-button/c-button.vue')['default']
'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default']
CButtonsSelect: typeof import('./src/ui/c-buttons-select/c-buttons-select.vue')['default']
'CButtonsSelect.demo': typeof import('./src/ui/c-buttons-select/c-buttons-select.demo.vue')['default']
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
CCollapse: typeof import('./src/ui/c-collapse/c-collapse.vue')['default']
'CCollapse.demo': typeof import('./src/ui/c-collapse/c-collapse.demo.vue')['default']
CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default']
CFileUpload: typeof import('./src/ui/c-file-upload/c-file-upload.vue')['default']
'CFileUpload.demo': typeof import('./src/ui/c-file-upload/c-file-upload.demo.vue')['default']
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default']
CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default']
CKeyValueListItem: typeof import('./src/ui/c-key-value-list/c-key-value-list-item.vue')['default']
CLabel: typeof import('./src/ui/c-label/c-label.vue')['default']
CLink: typeof import('./src/ui/c-link/c-link.vue')['default']
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
CMarkdown: typeof import('./src/ui/c-markdown/c-markdown.vue')['default']
'CMarkdown.demo': typeof import('./src/ui/c-markdown/c-markdown.demo.vue')['default']
CModal: typeof import('./src/ui/c-modal/c-modal.vue')['default']
'CModal.demo': typeof import('./src/ui/c-modal/c-modal.demo.vue')['default']
CModalValue: typeof import('./src/ui/c-modal-value/c-modal-value.vue')['default']
'CModalValue.demo': typeof import('./src/ui/c-modal-value/c-modal-value.demo.vue')['default']
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
CommandPalette: typeof import('./src/modules/command-palette/command-palette.vue')['default']
CommandPaletteOption: typeof import('./src/modules/command-palette/components/command-palette-option.vue')['default']
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
CSelect: typeof import('./src/ui/c-select/c-select.vue')['default']
'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default']
CTable: typeof import('./src/ui/c-table/c-table.vue')['default']
'CTable.demo': typeof import('./src/ui/c-table/c-table.demo.vue')['default']
CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default']
'CTextCopyable.demo': typeof import('./src/ui/c-text-copyable/c-text-copyable.demo.vue')['default']
CTooltip: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default']
'CTooltip.demo': typeof import('./src/ui/c-tooltip/c-tooltip.demo.vue')['default']
DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default']
'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default']
DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default']
DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default']
DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default']
DockerRunToDockerComposeConverter: typeof import('./src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue')['default']
DynamicValues: typeof import('./src/tools/benchmark-builder/dynamic-values.vue')['default']
Editor: typeof import('./src/tools/html-wysiwyg-editor/editor/editor.vue')['default']
EmojiCard: typeof import('./src/tools/emoji-picker/emoji-card.vue')['default']
EmojiGrid: typeof import('./src/tools/emoji-picker/emoji-grid.vue')['default']
EmojiPicker: typeof import('./src/tools/emoji-picker/emoji-picker.vue')['default']
Encryption: typeof import('./src/tools/encryption/encryption.vue')['default']
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default']
'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default']
HashText: typeof import('./src/tools/hash-text/hash-text.vue')['default']
HmacGenerator: typeof import('./src/tools/hmac-generator/hmac-generator.vue')['default']
'Home.page': typeof import('./src/pages/Home.page.vue')['default']
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default']
IconMdiArrowRight: typeof import('~icons/mdi/arrow-right')['default']
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
IconMdiClose: typeof import('~icons/mdi/close')['default']
IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
IconMdiDownload: typeof import('~icons/mdi/download')['default']
IconMdiEye: typeof import('~icons/mdi/eye')['default']
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
IconMdiHeart: typeof import('~icons/mdi/heart')['default']
IconMdiPause: typeof import('~icons/mdi/pause')['default']
IconMdiPlay: typeof import('~icons/mdi/play')['default']
IconMdiRecord: typeof import('~icons/mdi/record')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconMdiSearch: typeof import('~icons/mdi/search')['default']
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
IconMdiVideo: typeof import('~icons/mdi/video')['default']
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
Ipv4RangeExpander: typeof import('./src/tools/ipv4-range-expander/ipv4-range-expander.vue')['default']
Ipv4SubnetCalculator: typeof import('./src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue')['default']
Ipv6UlaGenerator: typeof import('./src/tools/ipv6-ula-generator/ipv6-ula-generator.vue')['default']
JsonDiff: typeof import('./src/tools/json-diff/json-diff.vue')['default']
JsonMinify: typeof import('./src/tools/json-minify/json-minify.vue')['default']
JsonToCsv: typeof import('./src/tools/json-to-csv/json-to-csv.vue')['default']
JsonToToml: typeof import('./src/tools/json-to-toml/json-to-toml.vue')['default']
JsonToYaml: typeof import('./src/tools/json-to-yaml-converter/json-to-yaml.vue')['default']
JsonViewer: typeof import('./src/tools/json-viewer/json-viewer.vue')['default']
JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default']
ListConverter: typeof import('./src/tools/list-converter/list-converter.vue')['default']
LocaleSelector: typeof import('./src/modules/i18n/components/locale-selector.vue')['default']
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default']
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default']
MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default']
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NAutoComplete: typeof import('naive-ui')['NAutoComplete']
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCode: typeof import('naive-ui')['NCode']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
@@ -37,37 +155,63 @@ declare module '@vue/runtime-core' {
NH1: typeof import('naive-ui')['NH1']
NH2: typeof import('naive-ui')['NH2']
NH3: typeof import('naive-ui')['NH3']
NH4: typeof import('naive-ui')['NH4']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu']
NP: typeof import('naive-ui')['NP']
NPageHeader: typeof import('naive-ui')['NPageHeader']
NProgress: typeof import('naive-ui')['NProgress']
NResult: typeof import('naive-ui')['NResult']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSlider: typeof import('naive-ui')['NSlider']
NSpace: typeof import('naive-ui')['NSpace']
NStatistic: typeof import('naive-ui')['NStatistic']
NSwitch: typeof import('naive-ui')['NSwitch']
NTable: typeof import('naive-ui')['NTable']
NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip']
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
NUpload: typeof import('naive-ui')['NUpload']
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
PdfSignatureChecker: typeof import('./src/tools/pdf-signature-checker/pdf-signature-checker.vue')['default']
PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default']
PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default']
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']
RomanNumeralConverter: typeof import('./src/tools/roman-numeral-converter/roman-numeral-converter.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
SearchBarItem: typeof import('./src/components/SearchBarItem.vue')['default']
RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default']
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default']
SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default']
TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default']
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
TextDiff: typeof import('./src/tools/text-diff/text-diff.vue')['default']
TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default']
TextToBinary: typeof import('./src/tools/text-to-binary/text-to-binary.vue')['default']
TextToNatoAlphabet: typeof import('./src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue')['default']
TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default']
'TokenGenerator.tool': typeof import('./src/tools/token-generator/token-generator.tool.vue')['default']
TomlToJson: typeof import('./src/tools/toml-to-json/toml-to-json.vue')['default']
TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.vue')['default']
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
UlidGenerator: typeof import('./src/tools/ulid-generator/ulid-generator.vue')['default']
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default']
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']
UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default']
UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default']
XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default']
YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default']
}
}

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="favicon.ico" />
<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" />
@@ -14,13 +14,13 @@
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="author" href="humans.txt" />
<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" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#18a058" />
<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" />
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#18a058" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />

71
locales/en.yml Normal file
View File

@@ -0,0 +1,71 @@
home:
categories:
newestTools: Newest tools
favoriteTools: 'Your favorite tools'
allTools: 'All the tools'
subtitle: 'Handy tools for developers'
toggleMenu: 'Toggle menu'
home: Home
uiLib: 'UI Lib'
support: 'Support IT Tools development'
buyMeACoffee: 'Buy me a coffee'
follow:
title: 'You like it-tools?'
p1: 'Give us a star on'
githubRepository: 'IT-Tools GitHub repository'
p2: 'or follow us on'
twitterAccount: 'IT-Tools Twitter account'
thankYou: 'Thank you !'
nav:
github: 'GitHub repository'
githubRepository: 'IT-Tools GitHub repository'
twitter: 'Twitter account'
twitterAccount: 'IT Tools Twitter account'
about: 'About IT-Tools'
aboutLabel: 'About'
darkMode: 'Dark mode'
lightMode: 'Light mode'
mode: 'Toggle dark/light mode'
about:
content: >
# About IT-Tools
This wonderful website, made with ❤ by [Corentin Thomasset](https://github.com/CorentinTh) , aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share it to people you think may find it useful too and don't forget to bookmark it in your shortcut bar!
IT Tools is open-source (under the MIT license) and free, and will always be, but it costs 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 [sponsoring me](https://www.buymeacoffee.com/cthmsst).
## Technologies
IT Tools is made in Vue.js (Vue 3) with the the Naive UI component library and is hosted and continuously deployed by Vercel. Third-party open-source libraries are used in some tools, you may find the complete list in the [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) file of the repository.
## Found a bug? A tool is missing?
If you need a tool that is currently not present here, and you think can be useful, you are welcome to submit a feature request in the [issues section](https://github.com/CorentinTh/it-tools/issues/new/choose) in the GitHub repository.
And if you found a bug, or something doesn't work as expected, please file a bug report in the [issues section](https://github.com/CorentinTh/it-tools/issues/new/choose) in the GitHub repository.
404:
notFound: '404 Not Found'
sorry: 'Sorry, this page does not seem to exist'
maybe: 'Maybe the cache is doing tricky things, try force-refreshing?'
backHome: 'Back home'
favoriteButton:
remove: 'Remove from favorites'
add: 'Add to favorites'
toolCard:
new: New
search:
label: Search
tools:
categories:
favorite-tools: 'Your favorite tools'
crypto: Crypto
converter: Converter
web: Web
images and videos: 'Images & Videos'
development: Development
network: Network
math: Math
measurement: Measurement
text: Text
data: Data

65
locales/fr.yml Normal file
View File

@@ -0,0 +1,65 @@
home:
categories:
newestTools: 'Les nouveaux outils'
favoriteTools: 'Vos outils favoris'
allTools: 'Tous les outils'
subtitle: 'Outils pour les développeurs'
toggleMenu: 'Menu'
home: Accueil
uiLib: 'UI Lib'
buyMeACoffee: 'Soutenez IT-Tools'
follow:
title: 'Vous aimez it-tools ?'
p1: 'Soutenez-nous avec une star sur'
githubRepository: "le dépôt GitHub d'IT-Tools"
p2: 'ou suivez-nous sur'
twitterAccount: "le compte Twitter d'IT-Tools"
thankYou: 'Merci !'
nav:
github: 'Dépôt GitHub'
githubRepository: "Dépôt GitHub d'IT-Tools"
twitter: 'Compte Twitter'
twitterAccount: "Compte Twitter d'IT-Tools"
about: "À propos d'IT-Tools"
aboutLabel: 'À propos'
darkMode: 'Mode sombre'
lightMode: 'Mode clair'
mode: 'Basculer le mode sombre/clair'
about:
content: >
# À propos de IT-Tools
Ce merveilleux site, fait avec ❤ par [Corentin Thomasset](https://github.com/CorentinTh), regroupe des outils utiles pour les développeurs et les personnes travaillant dans l'informatique. Si vous le trouvez utile, n'hésitez pas à le partager et n'oubliez pas de le mettre dans vos favoris !
IT Tools est open-source (sous licence MIT) et gratuit, et le restera toujours, mais cela me coûte de l'argent pour l'héberger et renouveler le nom de domaine. Si vous voulez soutenir mon travail, et m'encourager à ajouter plus d'outils, n'hésitez pas à me [soutenir](https://www.buymeacoffee.com/cthmsst).
## Technologies
IT Tools est fait en Vue.js (Vue 3) avec la bibliothèque de composants Naive UI et est hébergé et déployé en continu par Vercel. Des bibliothèques open-source tierces sont utilisées dans certains outils, vous pouvez trouver la liste complète dans le fichier [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) du dépôt.
## Vous avez trouvé un bug ? Un outil manque ?
Si vous avez besoin d'un outil qui n'est pas encore présent ici, et que vous pensez qu'il peut être utile, vous êtes invité à soumettre une demande de fonctionnalité dans la [section issue](https://github.com/CorentinTh/it-tools/issues/new/choose) du dépôt GitHub.
404:
notFound: '404 Not Found'
sorry: "Désolé, cette page n'existe pas"
maybe: 'Peut-être que le cache fait des siennes, essayez de forcer le rafraîchissement ?'
backHome: "Retour à l'accueil"
toolCard:
new: Nouveau
search:
label: Rechercher
tools:
categories:
favorite-tools: 'Vos outils favoris'
crypto: Cryptographie
converter: Convertisseur
web: Web
images and videos: 'Images & Vidéos'
development: Développement
network: Réseau
math: Math
measurement: Mesure
text: Texte
data: Données

71
locales/zh.yml Normal file
View File

@@ -0,0 +1,71 @@
home:
categories:
newestTools: '最新工具'
favoriteTools: '我的收藏'
allTools: '全部工具'
subtitle: '助力开发人员和 IT 工作者'
toggleMenu: '切换菜单'
home: '主页'
uiLib: 'UI 库'
support: '支持 IT 工具开发'
buyMeACoffee: '赞助'
follow:
title: '关注我们'
p1: '给我们 Star'
githubRepository: 'GitHub 仓库'
p2: '关注我们的'
twitterAccount: 'Twitter'
thankYou: '感谢您的支持!'
nav:
github: 'GitHub 仓库'
githubRepository: 'GitHub 仓库'
twitter: 'Twitter 账号'
twitterAccount: 'Twitter 账号'
about: '关于 IT-Tools'
aboutLabel: '关于'
darkMode: '深色模式'
lightMode: '浅色模式'
mode: '颜色模式'
about:
content: >
# 关于 IT-Tools
IT-Tools 由 [Corentin Thomasset](https://github.com/CorentinTh) 用 ❤ 开发,汇集了对开发人员和 IT 从业者有用的工具。如果对您有帮助,请将其分享给您的朋友,并且添加到收藏夹中!
IT-Tools 永久免费且开源MIT 许可证),但需要资金用于托管和续订域名。如果您想支持我的工作,并鼓励我添加更多工具,请考虑通过 [赞助我](https://www.buymeacoffee.com/cthmsst) 进行支持。
## 技术
IT-Tools 采用 Vue.jsVue 3和 Naive UI 组件库开发,并由 Vercel 托管和持续部署。某些工具使用了第三方开源库,您可以在仓库的 [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) 文件中找到完整的列表。
## 发现了 Bug缺少工具
如果目前这里没有您需要的工具,并且您认为它可能有用,欢迎在 GitHub 仓库的 [issues](https://github.com/CorentinTh/it-tools/issues/new/choose) 中提交新增功能的请求。
如果您发现了 Bug或者某些功能未能按预期工作请在 GitHub 仓库的 [issues](https://github.com/CorentinTh/it-tools/issues/new/choose) 中提交错误报告。
404:
notFound: '404 页面不存在'
sorry: '抱歉,该页面似乎不存在'
maybe: '也许缓存出现了一些问题,试试强制刷新页面?'
backHome: '返回主页'
favoriteButton:
remove: '取消收藏'
add: '加入收藏'
toolCard:
new: '新'
search:
label: '搜索'
tools:
categories:
favorite-tools: '我的收藏'
crypto: '加密'
converter: '转换器'
web: Web
images and videos: '图片和视频'
development: '开发'
network: '网络'
math: '数学'
measurement: '测量'
text: '文本'
data: '数据'

4
netlify.toml Normal file
View File

@@ -0,0 +1,4 @@
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

View File

@@ -1,6 +1,6 @@
{
"name": "it-tools",
"version": "2023.4.14-dbad773",
"version": "2023.12.21-5ed3693",
"description": "Collection of handy online tools for developers, with great UX. ",
"keywords": [
"productivity",
@@ -21,107 +21,119 @@
},
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"build": "vue-tsc --noEmit && NODE_OPTIONS=--max_old_space_size=4096 vite build",
"preview": "vite preview --port 5050",
"test": "npm run test:unit",
"test:unit": "vitest --environment jsdom",
"test:e2e": "playwright test",
"test:e2e:dev": "BASE_URL=http://localhost:5173 NO_WEB_SERVER=true playwright test",
"coverage": "vitest run --coverage",
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
"lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
"script:create-new-tool": "node scripts/create-tool.mjs",
"script:create:tool": "node scripts/create-tool.mjs",
"script:create:ui": "hygen generator ui-component",
"release": "node ./scripts/release.mjs"
},
"dependencies": {
"@it-tools/bip39": "^0.0.4",
"@it-tools/oggen": "^1.3.0",
"@sindresorhus/slugify": "^2.2.0",
"@tiptap/pm": "2.0.0-beta.220",
"@tiptap/starter-kit": "2.0.0-beta.220",
"@tiptap/vue-3": "2.0.0-beta.220",
"@sindresorhus/slugify": "^2.2.1",
"@tiptap/pm": "2.1.6",
"@tiptap/starter-kit": "2.1.6",
"@tiptap/vue-3": "2.0.3",
"@vicons/material": "^0.12.0",
"@vicons/tabler": "^0.12.0",
"@vueuse/core": "^8.9.4",
"@vueuse/head": "^0.7.13",
"@vueuse/router": "^9.13.0",
"@vueuse/core": "^10.3.0",
"@vueuse/head": "^1.0.0",
"@vueuse/router": "^10.0.0",
"bcryptjs": "^2.4.3",
"change-case": "^4.1.2",
"colord": "^2.9.3",
"composerize-ts": "^0.6.2",
"country-code-lookup": "^0.1.0",
"cron-validator": "^1.3.1",
"cronstrue": "^2.26.0",
"crypto-js": "^4.1.1",
"date-fns": "^2.29.3",
"dompurify": "^3.0.6",
"emojilib": "^3.0.10",
"figue": "^1.2.0",
"fuse.js": "^6.6.2",
"highlight.js": "^11.7.0",
"iarna-toml-esm": "^3.0.5",
"ibantools": "^4.3.3",
"json5": "^2.2.3",
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.28",
"lodash": "^4.17.21",
"mathjs": "^10.6.4",
"marked": "^10.0.0",
"mathjs": "^11.9.1",
"mime-types": "^2.1.35",
"naive-ui": "^2.34.3",
"monaco-editor": "^0.43.0",
"naive-ui": "^2.35.0",
"netmask": "^2.0.2",
"node-forge": "^1.3.1",
"oui": "^12.0.52",
"oui-data": "^1.0.10",
"pdf-signature-reader": "^1.4.2",
"pinia": "^2.0.34",
"plausible-tracker": "^0.3.8",
"qrcode": "^1.5.1",
"randombytes": "^2.1.0",
"sql-formatter": "^8.2.0",
"ts-pattern": "^4.2.2",
"sql-formatter": "^13.0.0",
"ua-parser-js": "^1.0.35",
"uuid": "^8.3.2",
"vue": "^3.2.47",
"ulid": "^2.3.0",
"unicode-emoji-json": "^0.4.0",
"unplugin-auto-import": "^0.16.4",
"uuid": "^9.0.0",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
"vue-tsc": "^1.8.1",
"xml-formatter": "^3.3.2",
"yaml": "^2.2.1"
},
"devDependencies": {
"@antfu/eslint-config": "^0.41.0",
"@iconify-json/mdi": "^1.1.50",
"@intlify/unplugin-vue-i18n": "^0.13.0",
"@playwright/test": "^1.32.3",
"@rushstack/eslint-patch": "^1.2.0",
"@tsconfig/node18": "^18.2.0",
"@types/bcryptjs": "^2.4.2",
"@types/crypto-js": "^4.1.1",
"@types/jsdom": "^16.2.15",
"@types/dompurify": "^3.0.5",
"@types/jsdom": "^21.0.0",
"@types/lodash": "^4.14.192",
"@types/mime-types": "^2.1.1",
"@types/netmask": "^2.0.0",
"@types/node": "^16.18.23",
"@types/node": "^18.15.11",
"@types/node-forge": "^1.3.2",
"@types/prettier": "^2.7.2",
"@types/qrcode": "^1.5.0",
"@types/randombytes": "^2.0.0",
"@types/ua-parser-js": "^0.7.36",
"@types/uuid": "^8.3.4",
"@typescript-eslint/parser": "^5.58.0",
"@unocss/eslint-config": "^0.50.8",
"@vitejs/plugin-vue": "^2.3.4",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^10.0.0",
"@types/uuid": "^9.0.0",
"@unocss/eslint-config": "^0.57.0",
"@vitejs/plugin-vue": "^4.3.2",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"@vue/compiler-sfc": "^3.2.47",
"@vue/runtime-dom": "^3.3.4",
"@vue/test-utils": "^2.3.2",
"@vue/tsconfig": "^0.1.3",
"c8": "^7.13.0",
"@vue/tsconfig": "^0.4.0",
"consola": "^3.0.2",
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-vue": "^8.7.1",
"jsdom": "^19.0.0",
"eslint": "^8.47.0",
"hygen": "^6.2.11",
"jsdom": "^22.0.0",
"less": "^4.1.3",
"prettier": "^2.8.7",
"start-server-and-test": "^1.15.4",
"typescript": "~4.5.5",
"unocss": "^0.50.8",
"unplugin-auto-import": "^0.15.2",
"unplugin-vue-components": "^0.24.1",
"vite": "^2.9.15",
"vite-plugin-md": "^0.12.4",
"vite-plugin-pwa": "^0.11.13",
"vite-svg-loader": "^3.6.0",
"vitest": "^0.13.1",
"vue-tsc": "^0.31.4",
"workbox-window": "^6.5.4",
"prettier": "^3.0.0",
"typescript": "~5.2.0",
"unocss": "^0.57.0",
"unocss-preset-scrollbar": "^0.2.1",
"unplugin-icons": "^0.17.0",
"unplugin-vue-components": "^0.25.0",
"vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.0",
"vite-plugin-vue-markdown": "^0.23.5",
"vite-svg-loader": "^4.0.0",
"vitest": "^0.34.0",
"workbox-window": "^7.0.0",
"zx": "^7.2.1"
}
}

View File

@@ -1,10 +1,8 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
const isCI = !!process.env.CI;
const baseUrl = process.env.BASE_URL || 'http://localhost:5050';
const useWebServer = process.env.NO_WEB_SERVER !== 'true';
/**
* See https://playwright.dev/docs/test-configuration.
@@ -15,17 +13,17 @@ export default defineConfig({
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
forbidOnly: isCI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
retries: isCI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
workers: isCI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://127.0.0.1:3000',
baseURL: baseUrl,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
@@ -51,32 +49,17 @@ export default defineConfig({
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run dev',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
...(useWebServer
&& {
webServer: {
command: 'npm run preview',
url: 'http://127.0.0.1:5050',
reuseExistingServer: !isCI,
},
}
),
});

8227
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
renovate.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
}

View File

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

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute, RouterView } from 'vue-router';
import { darkTheme, NGlobalStyle, NMessageProvider, NNotificationProvider } from 'naive-ui';
import { RouterView, useRoute } from 'vue-router';
import { NGlobalStyle, NMessageProvider, NNotificationProvider, darkTheme } from 'naive-ui';
import { darkThemeOverrides, lightThemeOverrides } from './themes';
import { layouts } from './layouts';
import { useStyleStore } from './stores/style.store';
@@ -12,18 +11,25 @@ const styleStore = useStyleStore();
const theme = computed(() => (styleStore.isDarkTheme ? darkTheme : null));
const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrides : lightThemeOverrides));
const { locale } = useI18n();
syncRef(
locale,
useStorage('locale', locale),
);
</script>
<template>
<n-config-provider :theme="theme" :theme-overrides="themeOverrides">
<n-global-style />
<n-message-provider placement="bottom">
<n-notification-provider placement="bottom-right">
<NGlobalStyle />
<NMessageProvider placement="bottom">
<NNotificationProvider placement="bottom-right">
<component :is="layout">
<router-view />
<RouterView />
</component>
</n-notification-provider>
</n-message-provider>
</NNotificationProvider>
</NMessageProvider>
</n-config-provider>
</template>

View File

@@ -1,39 +1,9 @@
<template>
<div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name">
<n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })">
<n-icon :component="ChevronRight" :class="{ rotated: isCollapsed }" size="16" />
<span>
{{ name }}
</span>
</n-text>
<n-collapse-transition :show="!isCollapsed">
<div class="menu-wrapper">
<div class="toggle-bar" @click="toggleCategoryCollapse({ name })" />
<n-menu
class="menu"
:value="(route.name as string)"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="tools"
:indent="8"
:default-expand-all="true"
/>
</div>
</n-collapse-transition>
</div>
</template>
<script setup lang="ts">
import type { Tool, ToolCategory } from '@/tools/tools.types';
import { ChevronRight } from '@vicons/tabler';
import { useStorage } from '@vueuse/core';
import { useThemeVars } from 'naive-ui';
import { toRefs, computed, h } from 'vue';
import { RouterLink, useRoute } from 'vue-router';
import MenuIconItem from './MenuIconItem.vue';
import type { Tool, ToolCategory } from '@/tools/tools.types';
const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] });
const { toolsByCategory } = toRefs(props);
@@ -49,8 +19,8 @@ const collapsedCategories = useStorage<Record<string, boolean>>(
{
deep: true,
serializer: {
read: (v) => (v ? JSON.parse(v) : null),
write: (v) => JSON.stringify(v),
read: v => (v ? JSON.parse(v) : null),
write: v => JSON.stringify(v),
},
},
);
@@ -61,12 +31,12 @@ function toggleCategoryCollapse({ name }: { name: string }) {
const menuOptions = computed(() =>
toolsByCategory.value.map(({ name, components }) => ({
name: name,
name,
isCollapsed: collapsedCategories.value[name],
tools: components.map((tool) => ({
tools: components.map(tool => ({
label: makeLabel(tool),
icon: makeIcon(tool),
key: tool.name,
key: tool.path,
})),
})),
);
@@ -74,27 +44,37 @@ const menuOptions = computed(() =>
const themeVars = useThemeVars();
</script>
<template>
<div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name">
<div ml-6px mt-12px flex cursor-pointer items-center op-60 @click="toggleCategoryCollapse({ name })">
<span :class="{ 'rotate-0': isCollapsed, 'rotate-90': !isCollapsed }" text-16px lh-1 op-50 transition-transform>
<icon-mdi-chevron-right />
</span>
<span ml-8px text-13px>
{{ name }}
</span>
</div>
<n-collapse-transition :show="!isCollapsed">
<div class="menu-wrapper">
<div class="toggle-bar" @click="toggleCategoryCollapse({ name })" />
<n-menu
class="menu"
:value="route.path"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="tools"
:indent="8"
:default-expand-all="true"
/>
</div>
</n-collapse-transition>
</div>
</template>
<style scoped lang="less">
.category-name {
font-size: 0.93em;
padding: 12px 0 0px 0;
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
.n-icon {
transition: transform ease 0.5s;
transform: rotate(90deg);
margin: 0 10px 0 7px;
opacity: 0.5;
&.rotated {
transform: rotate(0deg);
}
}
}
.menu-wrapper {
display: flex;
flex-direction: row;

View File

@@ -1,8 +1,13 @@
<script setup lang="ts">
import type { Component } from 'vue';
const props = defineProps<{ icon: Component; title: string }>();
const { icon, title } = toRefs(props);
</script>
<template>
<n-card class="colored-card">
<n-space justify="space-between" align="center">
<n-icon class="icon" size="40" :component="icon" />
</n-space>
<c-card class="colored-card">
<n-icon class="icon" size="40" :component="icon" />
<n-h3 class="title">
<n-ellipsis>{{ title }}</n-ellipsis>
</n-h3>
@@ -12,16 +17,9 @@
<slot />
</n-ellipsis>
</div>
</n-card>
</c-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);

View File

@@ -1,25 +1,11 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<n-button circle quaternary :type="buttonType" :style="{ opacity: isFavorite ? 1 : 0.2 }" @click="toggleFavorite">
<template #icon>
<n-icon :component="FavoriteFilled" />
</template>
</n-button>
</template>
{{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }}
</n-tooltip>
</template>
<script setup lang="ts">
import { FavoriteFilled } from '@vicons/material';
import { useToolStore } from '@/tools/tools.store';
import type { Tool } from '@/tools/tools.types';
import { computed, toRefs } from 'vue';
const props = defineProps<{ tool: Tool }>();
const toolStore = useToolStore();
const props = defineProps<{ tool: Tool }>();
const { tool } = toRefs(props);
const isFavorite = computed(() => toolStore.isToolFavorite({ tool }));
@@ -36,3 +22,17 @@ function toggleFavorite(event: MouseEvent) {
toolStore.addToolToFavorites({ tool });
}
</script>
<template>
<c-tooltip :tooltip="isFavorite ? $t('favoriteButton.remove') : $t('favoriteButton.add') ">
<c-button
variant="text"
circle
:type="buttonType"
:style="{ opacity: isFavorite ? 1 : 0.2 }"
@click="toggleFavorite"
>
<icon-mdi-heart />
</c-button>
</c-tooltip>
</template>

View File

@@ -1,36 +1,17 @@
<template>
<n-form-item :label="inputLabel" v-bind="validationAttrs">
<n-input
ref="inputElement"
v-model:value="input"
:placeholder="inputPlaceholder"
type="textarea"
rows="20"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
:input-props="{ 'data-test-id': 'input' }"
/>
</n-form-item>
<n-form-item :label="outputLabel">
<textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement" />
</n-form-item>
</template>
<script setup lang="ts">
import { useValidation, type UseValidationRule } from '@/composable/validation';
import _ from 'lodash';
import type { UseValidationRule } from '@/composable/validation';
import CInputText from '@/ui/c-input-text/c-input-text.vue';
const props = withDefaults(
defineProps<{
transformer?: (v: string) => string;
inputValidationRules?: UseValidationRule<string>[];
inputLabel?: string;
inputPlaceholder?: string;
inputDefault?: string;
outputLabel?: string;
outputLanguage?: string;
transformer?: (v: string) => string
inputValidationRules?: UseValidationRule<string>[]
inputLabel?: string
inputPlaceholder?: string
inputDefault?: string
outputLabel?: string
outputLanguage?: string
}>(),
{
transformer: _.identity,
@@ -43,15 +24,34 @@ const props = withDefaults(
},
);
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } =
toRefs(props);
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault }
= toRefs(props);
const inputElement = ref();
const inputElement = ref<typeof CInputText>();
const input = ref(inputDefault.value);
const output = computed(() => transformer.value(input.value));
const { attrs: validationAttrs } = useValidation({ source: input, rules: inputValidationRules.value });
</script>
<style scoped></style>
<template>
<CInputText
ref="inputElement"
v-model:value="input"
:placeholder="inputPlaceholder"
:label="inputLabel"
rows="20"
autosize
raw-text
multiline
test-id="input"
:validation-rules="inputValidationRules"
monospace
/>
<div>
<div mb-5px>
{{ outputLabel }}
</div>
<textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" />
</div>
</template>

View File

@@ -1,43 +1,23 @@
<template>
<n-input v-model:value="value">
<template #suffix>
<n-tooltip trigger="hover">
<template #trigger>
<n-button quaternary circle @click="onCopyClicked">
<n-icon :component="ContentCopyFilled" />
</n-button>
</template>
{{ tooltipText }}
</n-tooltip>
</template>
</n-input>
</template>
<script setup lang="ts">
import { useVModel, useClipboard } from '@vueuse/core';
import { ContentCopyFilled } from '@vicons/material';
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { useCopy } from '@/composable/copy';
const props = defineProps<{ value: string }>();
const emit = defineEmits(['update:value']);
const value = useVModel(props, 'value', emit);
const tooltipText = ref('Copy to clipboard');
const { copy } = useClipboard({ source: value });
function onCopyClicked() {
copy();
tooltipText.value = 'Copied !';
setTimeout(() => {
tooltipText.value = 'Copy to clipboard';
}, 2000);
}
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : 'Copy to clipboard');
</script>
<style scoped>
::v-deep(.n-input-wrapper) {
padding-right: 5px;
}
</style>
<template>
<c-input-text v-model:value="value">
<template #suffix>
<c-tooltip :tooltip="tooltipText">
<c-button circle variant="text" size="small" @click="copy()">
<icon-mdi-content-copy />
</c-button>
</c-tooltip>
</template>
</c-input-text>
</template>

View File

@@ -1,14 +1,6 @@
<template>
<div class="menu-icon-item">
<n-icon :component="tool.icon" />
<div v-if="tool.isNew" class="badge"></div>
</div>
</template>
<script setup lang="ts">
import type { Tool } from '@/tools/tools.types';
import { useThemeVars } from 'naive-ui';
import { toRefs } from 'vue';
import type { Tool } from '@/tools/tools.types';
const props = defineProps<{ tool: Tool }>();
const { tool } = toRefs(props);
@@ -16,6 +8,13 @@ const { tool } = toRefs(props);
const theme = useThemeVars();
</script>
<template>
<div class="menu-icon-item">
<n-icon :component="tool.icon" />
<div v-if="tool.isNew" class="badge" />
</div>
</template>
<style lang="less" scoped>
.menu-icon-item {
position: relative;

View File

@@ -1,3 +1,11 @@
<script setup lang="ts">
import { useStyleStore } from '@/stores/style.store';
const styleStore = useStyleStore();
const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore);
const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static'));
</script>
<template>
<n-layout has-sider>
<n-layout-sider
@@ -19,15 +27,6 @@
</n-layout>
</template>
<script setup lang="ts">
import { useStyleStore } from '@/stores/style.store';
import { toRefs, computed } from 'vue';
const styleStore = useStyleStore();
const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore);
const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static'));
</script>
<style lang="less" scoped>
.overlay {
position: absolute;

View File

@@ -1,71 +1,51 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<n-button
size="large"
circle
quaternary
tag="a"
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>
</template>
Github repository
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<n-button
size="large"
circle
quaternary
tag="a"
href="https://twitter.com/ittoolsdottech"
rel="noopener"
target="_blank"
aria-label="IT Tools' Twitter account"
>
<n-icon size="25" :component="BrandTwitter" />
</n-button>
</template>
IT Tools' Twitter account
</n-tooltip>
<router-link to="/about" #="{ navigate, href }" custom>
<n-tooltip trigger="hover">
<template #trigger>
<n-button tag="a" :href="href" circle quaternary size="large" aria-label="About" @click="navigate">
<n-icon size="25" :component="InfoCircle" />
</n-button>
</template>
About
</n-tooltip>
</router-link>
<n-tooltip trigger="hover">
<template #trigger>
<n-button size="large" circle quaternary aria-label="Toggle dark/light mode" @click="isDarkTheme = !isDarkTheme">
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
<n-icon v-else size="25" :component="Moon" />
</n-button>
</template>
<span v-if="isDarkTheme">Light mode</span>
<span v-else>Dark mode</span>
</n-tooltip>
</template>
<script setup lang="ts">
import { useStyleStore } from '@/stores/style.store';
import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler';
import { toRefs } from 'vue';
import { useStyleStore } from '@/stores/style.store';
const styleStore = useStyleStore();
const { isDarkTheme } = toRefs(styleStore);
</script>
<template>
<c-tooltip :tooltip="$t('home.nav.github')" position="bottom">
<c-button
circle
variant="text"
href="https://github.com/CorentinTh/it-tools"
target="_blank"
rel="noopener noreferrer"
:aria-label="$t('home.nav.githubRepository')"
>
<n-icon size="25" :component="BrandGithub" />
</c-button>
</c-tooltip>
<c-tooltip :tooltip="$t('home.nav.twitter')" position="bottom">
<c-button
circle
variant="text"
href="https://twitter.com/ittoolsdottech"
rel="noopener"
target="_blank"
:aria-label="$t('home.nav.twitterAccount')"
>
<n-icon size="25" :component="BrandTwitter" />
</c-button>
</c-tooltip>
<c-tooltip :tooltip="$t('home.nav.about')" position="bottom">
<c-button circle variant="text" to="/about" :aria-label="$t('home.nav.aboutLabel')">
<n-icon size="25" :component="InfoCircle" />
</c-button>
</c-tooltip>
<c-tooltip :tooltip="isDarkTheme ? $t('home.nav.lightMode') : $t('home.nav.darkMode')" position="bottom">
<c-button circle variant="text" :aria-label="$t('home.nav.mode')" @click="() => styleStore.toggleDark()">
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
<n-icon v-else size="25" :component="Moon" />
</c-button>
</c-tooltip>
</template>
<style lang="less" scoped>
.n-button {
&:not(:last-child) {

View File

@@ -1,110 +0,0 @@
<script lang="ts" setup>
import { useFuzzySearch } from '@/composable/fuzzySearch';
import { useTracker } from '@/modules/tracker/tracker.services';
import { tools } from '@/tools';
import type { Tool } from '@/tools/tools.types';
import { SearchRound } from '@vicons/material';
import { useMagicKeys, whenever } from '@vueuse/core';
import type { NInput } from 'naive-ui';
import { computed, h, ref } from 'vue';
import { useRouter } from 'vue-router';
import SearchBarItem from './SearchBarItem.vue';
const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool });
const router = useRouter();
const { tracker } = useTracker();
const queryString = ref('');
const inputEl = ref<HTMLElement>();
const displayDropDown = ref(true);
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
const options = computed(() => {
if (queryString.value === '') {
return tools.map(toolToOption);
}
return searchResult.value.map(toolToOption);
});
const { searchResult } = useFuzzySearch({
search: queryString,
data: tools,
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
});
const keys = useMagicKeys({
passive: false,
onEventFired(e) {
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') {
e.preventDefault();
}
if (e.metaKey && e.key === 'k' && e.type === 'keydown') {
e.preventDefault();
}
},
});
whenever(keys.ctrl_k, claimFocus);
whenever(keys.meta_k, claimFocus);
whenever(keys.escape, releaseFocus);
function renderOption({ tool }: { tool: Tool }) {
return h(SearchBarItem, { tool });
}
function onSelect(path: string) {
router.push(path);
queryString.value = '';
}
function claimFocus() {
displayDropDown.value = true;
inputEl.value?.focus();
}
function releaseFocus() {
displayDropDown.value = false;
}
function onFocus() {
tracker.trackEvent({ eventName: 'Search-bar focused' });
displayDropDown.value = true;
}
</script>
<template>
<div class="search-bar">
<n-auto-complete
v-model:value="queryString"
:options="options"
:on-select="(value) => onSelect(String(value))"
:render-label="renderOption"
:default-value="'aa'"
:get-show="() => displayDropDown"
:on-focus="onFocus"
@update:value="() => (displayDropDown = true)"
>
<template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
<n-input
ref="inputEl"
round
clearable
:placeholder="`Search a tool (use ${isMac ? 'Cmd' : 'Ctrl'} + K to focus)`"
:value="slotValue"
:input-props="{ autocomplete: 'disabled' }"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
>
<template #prefix>
<n-icon :component="SearchRound" />
</template>
</n-input>
</template>
</n-auto-complete>
</div>
</template>

View File

@@ -1,45 +0,0 @@
<script lang="ts" setup>
import type { Tool } from '@/tools/tools.types';
import { toRefs } from 'vue';
const props = defineProps<{ tool: Tool }>();
const { tool } = toRefs(props);
</script>
<template>
<div class="search-bar-item">
<n-icon class="icon" :component="tool.icon" />
<div>
<div class="name">{{ tool.name }}</div>
<div class="description">{{ tool.description }}</div>
</div>
</div>
</template>
<style lang="less" scoped>
.search-bar-item {
padding: 10px;
display: flex;
flex-direction: row;
align-items: center;
.icon {
font-size: 30px;
margin-right: 10px;
opacity: 0.7;
}
.name {
font-weight: bold;
font-size: 15px;
line-height: 1;
margin-bottom: 5px;
}
.description {
opacity: 0.7;
line-height: 1;
}
}
</style>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
const props = withDefaults(defineProps<{ value?: string }>(), { value: '' });
const { value } = toRefs(props);
const initialText = 'Copy to clipboard';
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : initialText);
</script>
<template>
<c-tooltip :tooltip="tooltipText">
<span cursor-pointer font-mono @click="copy()">{{ value }}</span>
</c-tooltip>
</template>

View File

@@ -1,6 +1,46 @@
<script setup lang="ts">
import { Copy } from '@vicons/tabler';
import { useElementSize } from '@vueuse/core';
import hljs from 'highlight.js/lib/core';
import jsonHljs from 'highlight.js/lib/languages/json';
import sqlHljs from 'highlight.js/lib/languages/sql';
import xmlHljs from 'highlight.js/lib/languages/xml';
import yamlHljs from 'highlight.js/lib/languages/yaml';
import iniHljs from 'highlight.js/lib/languages/ini';
import { useCopy } from '@/composable/copy';
const props = withDefaults(
defineProps<{
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',
},
);
hljs.registerLanguage('sql', sqlHljs);
hljs.registerLanguage('json', jsonHljs);
hljs.registerLanguage('html', xmlHljs);
hljs.registerLanguage('xml', xmlHljs);
hljs.registerLanguage('yaml', yamlHljs);
hljs.registerLanguage('toml', iniHljs);
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) };
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.value);
</script>
<template>
<div style="overflow-x: hidden; width: 100%">
<n-card class="result-card">
<c-card relative>
<n-scrollbar
x-scrollable
trigger="none"
@@ -10,93 +50,25 @@
<n-code :code="value" :language="language" :trim="false" data-test-id="area-content" />
</n-config-provider>
</n-scrollbar>
<n-tooltip v-if="value" trigger="hover">
<template #trigger>
<div class="copy-button" :class="[copyPlacement]">
<n-button circle secondary 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" mt-4>
<n-button secondary @click="onCopyClicked"> {{ tooltipText }} </n-button>
</n-space>
<div absolute right-10px top-10px>
<c-tooltip v-if="value" :tooltip="tooltipText" position="left">
<c-button circle important:h-10 important:w-10 @click="copy()">
<n-icon size="22" :component="Copy" />
</c-button>
</c-tooltip>
</div>
</c-card>
<div v-if="copyPlacement === 'outside'" mt-4 flex justify-center>
<c-button @click="copy()">
{{ tooltipText }}
</c-button>
</div>
</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 yamlHljs from 'highlight.js/lib/languages/yaml';
import { ref, toRefs } from 'vue';
hljs.registerLanguage('sql', sqlHljs);
hljs.registerLanguage('json', jsonHljs);
hljs.registerLanguage('html', xmlHljs);
hljs.registerLanguage('yaml', yamlHljs);
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>

View File

@@ -1,9 +1,22 @@
<script setup lang="ts">
import { useThemeVars } from 'naive-ui';
import FavoriteButton from './FavoriteButton.vue';
import { useAppTheme } from '@/ui/theme/themes';
import type { Tool } from '@/tools/tools.types';
const props = defineProps<{ tool: Tool & { category: string } }>();
const { tool } = toRefs(props);
const theme = useThemeVars();
const appTheme = useAppTheme();
</script>
<template>
<router-link :to="tool.path">
<n-card class="tool-card">
<n-space justify="space-between" align="center">
<c-card class="tool-card">
<div flex items-center justify-between>
<n-icon class="icon" size="40" :component="tool.icon" />
<n-space align="center">
<div flex items-center gap-8px>
<n-tag
v-if="tool.isNew"
size="small"
@@ -13,12 +26,12 @@
:bordered="false"
:color="{ color: theme.primaryColor, textColor: theme.tagColor }"
>
New
{{ $t('toolCard.new') }}
</n-tag>
<favorite-button :tool="tool" />
</n-space>
</n-space>
<FavoriteButton :tool="tool" />
</div>
</div>
<n-h3 class="title">
<n-ellipsis>{{ tool.name }}</n-ellipsis>
</n-h3>
@@ -26,32 +39,25 @@
<div class="description">
<n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px">
{{ tool.description }}
<br />&nbsp;
<br>&nbsp;
</n-ellipsis>
</div>
</n-card>
</c-card>
</router-link>
</template>
<script setup lang="ts">
import type { Tool } from '@/tools/tools.types';
import { useThemeVars } from 'naive-ui';
import { toRefs } from 'vue';
import FavoriteButton from './FavoriteButton.vue';
const props = defineProps<{ tool: Tool & { category: string } }>();
const { tool } = toRefs(props);
const theme = useThemeVars();
</script>
<style lang="less" scoped>
a {
text-decoration: none;
}
.tool-card {
transition: border-color ease 0.5s;
border-width: 2px !important;
color: transparent;
&:hover {
border-color: var(--n-color-target);
border-color: v-bind('appTheme.primary.colorHover');
}
.icon {

View File

@@ -0,0 +1,22 @@
import { type Ref, ref, watchEffect } from 'vue';
export { computedCatch };
function computedCatch<T, D>(getter: () => T, { defaultValue }: { defaultValue: D; defaultErrorMessage?: string }): [Ref<T | D>, Ref<string | undefined>];
function computedCatch<T, D>(getter: () => T, { defaultValue, defaultErrorMessage = 'Unknown error' }: { defaultValue?: D; defaultErrorMessage?: string } = {}) {
const error = ref<string | undefined>();
const value = ref<T | D | undefined>();
watchEffect(() => {
try {
error.value = undefined;
value.value = getter();
}
catch (err) {
error.value = err instanceof Error ? err.message : err?.toString() ?? defaultErrorMessage;
value.value = defaultValue;
}
});
return [value, error] as const;
}

View File

@@ -11,7 +11,8 @@ function computedRefreshable<T>(getter: () => T, { throttle }: { throttle?: numb
if (throttle) {
watchThrottled(getter, update, { throttle });
} else {
}
else {
watch(getter, update);
}

View File

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

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';
import { getMimeTypeFromBase64 } from './downloadBase64';
describe('downloadBase64', () => {
describe('getMimeTypeFromBase64', () => {
it('when the base64 string has a data URI, it returns the mime type', () => {
expect(getMimeTypeFromBase64({ base64String: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/png' });
expect(getMimeTypeFromBase64({ base64String: 'data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/jpg' });
});
it('when the base64 string has no data URI, it try to infer the mime type from the signature', () => {
// https://en.wikipedia.org/wiki/List_of_file_signatures
// PNG
expect(getMimeTypeFromBase64({ base64String: 'iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/png' });
// GIF
expect(getMimeTypeFromBase64({ base64String: 'R0lGODdh' })).to.deep.equal({ mimeType: 'image/gif' });
expect(getMimeTypeFromBase64({ base64String: 'R0lGODlh' })).to.deep.equal({ mimeType: 'image/gif' });
// JPG
expect(getMimeTypeFromBase64({ base64String: '/9j/' })).to.deep.equal({ mimeType: 'image/jpg' });
// PDF
expect(getMimeTypeFromBase64({ base64String: 'JVBERi0' })).to.deep.equal({ mimeType: 'application/pdf' });
});
it('when the base64 string has no data URI and no signature, it returns an undefined mimeType', () => {
expect(getMimeTypeFromBase64({ base64String: 'JVBERi' })).to.deep.equal({ mimeType: undefined });
});
});
});

View File

@@ -1,32 +1,60 @@
import { extension as getExtensionFromMime } from 'mime-types';
import type { Ref } from 'vue';
import _ from 'lodash';
function getFileExtensionFromBase64({
base64String,
export { getMimeTypeFromBase64, useDownloadFileFromBase64 };
const commonMimeTypesSignatures = {
'JVBERi0': 'application/pdf',
'R0lGODdh': 'image/gif',
'R0lGODlh': 'image/gif',
'iVBORw0KGgo': 'image/png',
'/9j/': 'image/jpg',
};
function getMimeTypeFromBase64({ base64String }: { base64String: string }) {
const [,mimeTypeFromBase64] = base64String.match(/data:(.*?);base64/i) ?? [];
if (mimeTypeFromBase64) {
return { mimeType: mimeTypeFromBase64 };
}
const inferredMimeType = _.find(commonMimeTypesSignatures, (_mimeType, signature) => base64String.startsWith(signature));
if (inferredMimeType) {
return { mimeType: inferredMimeType };
}
return { mimeType: undefined };
}
function getFileExtensionFromMimeType({
mimeType,
defaultExtension = 'txt',
}: {
base64String: string;
defaultExtension?: string;
mimeType: string | undefined
defaultExtension?: string
}) {
const hasMimeType = base64String.match(/data:(.*?);base64/i);
if (hasMimeType) {
return getExtensionFromMime(hasMimeType[1]) || defaultExtension;
if (mimeType) {
return getExtensionFromMime(mimeType) ?? defaultExtension;
}
return defaultExtension;
}
export function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
return {
download() {
const base64String = source.value;
if (base64String === '') {
if (source.value === '') {
throw new Error('Base64 string is empty');
}
const cleanFileName = filename ?? `file.${getFileExtensionFromBase64({ base64String })}`;
const { mimeType } = getMimeTypeFromBase64({ base64String: source.value });
const base64String = mimeType
? source.value
: `data:text/plain;base64,${source.value}`;
const cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`;
const a = document.createElement('a');
a.href = base64String;

View File

@@ -1,4 +1,4 @@
import { get, type MaybeRef } from '@vueuse/core';
import { type MaybeRef, get } from '@vueuse/core';
import Fuse from 'fuse.js';
import { computed } from 'vue';
@@ -9,14 +9,21 @@ function useFuzzySearch<Data>({
data,
options = {},
}: {
search: MaybeRef<string>;
data: Data[];
options?: Fuse.IFuseOptions<Data>;
search: MaybeRef<string>
data: Data[]
options?: Fuse.IFuseOptions<Data> & { filterEmpty?: boolean }
}) {
const fuse = new Fuse(data, options);
const filterEmpty = options.filterEmpty ?? true;
const searchResult = computed(() => {
return fuse.search(get(search)).map(({ item }) => item);
const searchResult = computed<Data[]>(() => {
const query = get(search);
if (!filterEmpty && query === '') {
return data;
}
return fuse.search(query).map(({ item }) => item);
});
return { searchResult };

View File

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

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { describe, expect, it } from 'vitest';
import { isFalsyOrHasThrown } from './validation';
@@ -11,7 +10,7 @@ describe('useValidation', () => {
expect(isFalsyOrHasThrown(() => {})).toBe(true);
expect(
isFalsyOrHasThrown(() => {
throw new Error();
throw new Error('message');
}),
).toBe(true);
});

View File

@@ -1,44 +1,48 @@
import { type MaybeRef, get } from '@vueuse/core';
import _ from 'lodash';
import { reactive, watch, type Ref } from 'vue';
import { type Ref, reactive, watch } from 'vue';
type ValidatorReturnType = unknown;
export interface UseValidationRule<T> {
validator: (value: T) => ValidatorReturnType;
message: string;
validator: (value: T) => ValidatorReturnType
message: string
}
export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
try {
const returnValue = cb();
if (_.isNil(returnValue)) return true;
if (_.isNil(returnValue)) {
return true;
}
return returnValue === false;
} catch (_) {
}
catch (_) {
return true;
}
}
export type ValidationAttrs = {
feedback: string;
validationStatus: string | undefined;
};
export interface ValidationAttrs {
feedback: string
validationStatus: string | undefined
}
export function useValidation<T>({
source,
rules,
watch: watchRefs = [],
}: {
source: Ref<T>;
rules: UseValidationRule<T>[];
watch?: Ref<unknown>[];
source: Ref<T>
rules: MaybeRef<UseValidationRule<T>[]>
watch?: Ref<unknown>[]
}) {
const state = reactive<{
message: string;
status: undefined | 'error';
isValid: boolean;
attrs: ValidationAttrs;
message: string
status: undefined | 'error'
isValid: boolean
attrs: ValidationAttrs
}>({
message: '',
status: undefined,
@@ -55,7 +59,7 @@ export function useValidation<T>({
state.message = '';
state.status = undefined;
for (const rule of rules) {
for (const rule of get(rules)) {
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
state.message = rule.message;
state.status = 'error';

View File

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

View File

@@ -1,19 +1,19 @@
<script lang="ts" setup>
import { NIcon, useThemeVars } from 'naive-ui';
import { computed } from 'vue';
import { RouterLink } from 'vue-router';
import { Heart, Menu2, Home2 } from '@vicons/tabler';
import { toolsByCategory } from '@/tools';
import { Heart, Home2, Menu2 } from '@vicons/tabler';
import { storeToRefs } from 'pinia';
import HeroGradient from '../assets/hero-gradient.svg?component';
import MenuLayout from '../components/MenuLayout.vue';
import NavbarButtons from '../components/NavbarButtons.vue';
import { useStyleStore } from '@/stores/style.store';
import { config } from '@/config';
import type { ToolCategory } from '@/tools/tools.types';
import { useToolStore } from '@/tools/tools.store';
import { useTracker } from '@/modules/tracker/tracker.services';
import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue';
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 styleStore = useStyleStore();
@@ -21,133 +21,123 @@ const version = config.app.version;
const commitSha = config.app.lastCommitSha.slice(0, 7);
const { tracker } = useTracker();
const { t } = useI18n();
const toolStore = useToolStore();
const { favoriteTools, toolsByCategory } = storeToRefs(toolStore);
const tools = computed<ToolCategory[]>(() => [
...(toolStore.favoriteTools.length > 0 ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }] : []),
...toolsByCategory,
...(favoriteTools.value.length > 0 ? [{ name: t('tools.categories.favorite-tools'), components: favoriteTools.value }] : []),
...toolsByCategory.value,
]);
</script>
<template>
<menu-layout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }">
<MenuLayout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }">
<template #sider>
<router-link to="/" class="hero-wrapper">
<hero-gradient class="gradient" />
<RouterLink to="/" class="hero-wrapper">
<HeroGradient class="gradient" />
<div class="text-wrapper">
<div class="title">IT - TOOLS</div>
<div class="title">
IT - TOOLS
</div>
<div class="divider" />
<div class="subtitle">Handy tools for developers</div>
<div class="subtitle">
{{ $t('home.subtitle') }}
</div>
</div>
</router-link>
</RouterLink>
<div class="sider-content">
<n-space v-if="styleStore.isSmallScreen" justify="center">
<navbar-buttons />
</n-space>
<div v-if="styleStore.isSmallScreen" flex flex-col items-center>
<locale-selector w="90%" />
<collapsible-tool-menu :tools-by-category="tools" />
<div flex justify-center>
<NavbarButtons />
</div>
</div>
<CollapsibleToolMenu :tools-by-category="tools" />
<div class="footer">
<div>
IT-Tools
<n-button
text
tag="a"
target="_blank"
rel="noopener"
type="primary"
depth="3"
:href="`https://github.com/CorentinTh/it-tools/tree/v${version}`"
>
<c-link target="_blank" rel="noopener" :href="`https://github.com/CorentinTh/it-tools/tree/v${version}`">
v{{ version }}
</n-button>
</c-link>
<template v-if="commitSha && commitSha.length > 0">
-
<n-button
text
tag="a"
<c-link
target="_blank"
rel="noopener"
type="primary"
depth="3"
:href="`https://github.com/CorentinTh/it-tools/tree/${commitSha}`"
>
{{ commitSha }}
</n-button>
</c-link>
</template>
</div>
<div>
© {{ new Date().getFullYear() }}
<n-button text tag="a" target="_blank" rel="noopener" type="primary" href="https://github.com/CorentinTh">
<c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh">
Corentin Thomasset
</n-button>
</c-link>
</div>
</div>
</div>
</template>
<template #content>
<div class="navigation">
<n-button
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
<div flex items-center justify-center gap-2>
<c-button
circle
quaternary
aria-label="Toggle menu"
variant="text"
:aria-label="$t('home.toggleMenu')"
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
>
<n-icon size="25" :component="Menu2" />
</n-button>
<NIcon size="25" :component="Menu2" />
</c-button>
<router-link to="/" #="{ navigate, href }" custom>
<n-tooltip trigger="hover">
<template #trigger>
<n-button
tag="a"
:href="href"
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
circle
quaternary
aria-label="Home"
@click="navigate"
>
<n-icon size="25" :component="Home2" />
</n-button>
</template>
Home
</n-tooltip>
</router-link>
<c-tooltip :tooltip="$t('home.home')" position="bottom">
<c-button to="/" circle variant="text" :aria-label="$t('home.home')">
<NIcon size="25" :component="Home2" />
</c-button>
</c-tooltip>
<search-bar />
<c-tooltip :tooltip="$t('home.uiLib')" position="bottom">
<c-button v-if="config.app.env === 'development'" to="/c-lib" circle variant="text" :aria-label="$t('home.uiLib')">
<icon-mdi:brush-variant text-20px />
</c-button>
</c-tooltip>
<navbar-buttons v-if="!styleStore.isSmallScreen" />
<command-palette />
<n-tooltip trigger="hover">
<template #trigger>
<n-button
round
type="primary"
tag="a"
href="https://www.buymeacoffee.com/cthmsst"
rel="noopener"
target="_blank"
class="support-button"
:bordered="false"
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
>
Buy me a coffee
<n-icon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
</n-button>
</template>
Support IT Tools development !
</n-tooltip>
<locale-selector v-if="!styleStore.isSmallScreen" />
<div>
<NavbarButtons v-if="!styleStore.isSmallScreen" />
</div>
<c-tooltip position="bottom" :tooltip="$t('home.support')">
<c-button
round
href="https://www.buymeacoffee.com/cthmsst"
rel="noopener"
target="_blank"
class="support-button"
:bordered="false"
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
>
{{ $t('home.buyMeACoffee') }}
<NIcon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
</c-button>
</c-tooltip>
</div>
<slot />
</template>
</menu-layout>
</MenuLayout>
</template>
<style lang="less" scoped>
@@ -165,8 +155,8 @@ const tools = computed<ToolCategory[]>(() => [
.support-button {
background: rgb(37, 99, 108);
background: linear-gradient(48deg, rgba(37, 99, 108, 1) 0%, rgba(59, 149, 111, 1) 60%, rgba(20, 160, 88, 1) 100%);
color: #fff;
transition: all ease 0.2s;
color: #fff !important;
transition: padding ease 0.2s !important;
&:hover {
color: #fff;
@@ -225,25 +215,4 @@ const tools = computed<ToolCategory[]>(() => [
}
}
}
// ::v-deep(.n-menu-item-content-header) {
// overflow: visible !important;
// // overflow-x: hidden !important;
// }
.navigation {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
& > *:not(:last-child) {
margin-right: 5px;
}
.search-bar {
// width: 100%;
flex-grow: 1;
}
}
</style>

View File

@@ -2,10 +2,10 @@
import { useRoute } from 'vue-router';
import { useHead } from '@vueuse/head';
import type { HeadObject } from '@vueuse/head';
import { computed } from 'vue';
import BaseLayout from './base.layout.vue';
import FavoriteButton from '@/components/FavoriteButton.vue';
import type { Tool } from '@/tools/tools.types';
import BaseLayout from './base.layout.vue';
const route = useRoute();
@@ -23,26 +23,31 @@ const head = computed<HeadObject>(() => ({
],
}));
useHead(head);
const { t } = useI18n();
const i18nKey = computed<string>(() => route.path.trim().replace('/', ''));
const toolTitle = computed<string>(() => t(`tools.${i18nKey.value}.title`, String(route.meta.name)));
const toolDescription = computed<string>(() => t(`tools.${i18nKey.value}.description`, String(route.meta.description)));
</script>
<template>
<base-layout>
<BaseLayout>
<div class="tool-layout">
<div class="tool-header">
<n-space align="center" justify="space-between" :wrap="false">
<div flex flex-nowrap items-center justify-between>
<n-h1>
{{ route.meta.name }}
{{ toolTitle }}
</n-h1>
<div>
<favorite-button :tool="{name: route.meta.name} as Tool" />
<FavoriteButton :tool="{ name: route.meta.name } as Tool" />
</div>
</n-space>
</div>
<div class="separator" />
<div class="description">
{{ route.meta.description }}
{{ toolDescription }}
</div>
</div>
</div>
@@ -50,7 +55,7 @@ useHead(head);
<div class="tool-content">
<slot />
</div>
</base-layout>
</BaseLayout>
</template>
<style lang="less" scoped>

View File

@@ -1,23 +1,25 @@
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';
import 'virtual:uno.css';
registerSW();
import { naive } from './plugins/naive.plugin';
import App from './App.vue';
import router from './router';
import { i18nPlugin } from './plugins/i18n.plugin';
registerSW();
const app = createApp(App);
app.use(createPinia());
app.use(createHead());
app.use(i18nPlugin);
app.use(router);
app.use(naive);
app.use(plausible);

View File

@@ -0,0 +1,91 @@
import { defineStore } from 'pinia';
import _ from 'lodash';
import type { PaletteOption } from './command-palette.types';
import { useToolStore } from '@/tools/tools.store';
import { useFuzzySearch } from '@/composable/fuzzySearch';
import { useStyleStore } from '@/stores/style.store';
import SunIcon from '~icons/mdi/white-balance-sunny';
import GithubIcon from '~icons/mdi/github';
import BugIcon from '~icons/mdi/bug-outline';
import DiceIcon from '~icons/mdi/dice-5';
import InfoIcon from '~icons/mdi/information-outline';
export const useCommandPaletteStore = defineStore('command-palette', () => {
const toolStore = useToolStore();
const styleStore = useStyleStore();
const router = useRouter();
const searchPrompt = ref('');
const toolsOptions = toolStore.tools.map(tool => ({
...tool,
to: tool.path,
toolCategory: tool.category,
category: 'Tools',
}));
const searchOptions: PaletteOption[] = [
...toolsOptions,
{
name: 'Random tool',
description: 'Get a random tool from the list.',
action: () => {
const { path } = _.sample(toolStore.tools)!;
router.push(path);
},
icon: DiceIcon,
category: 'Tools',
keywords: ['random', 'tool', 'pick', 'choose', 'select'],
closeOnSelect: true,
},
{
name: 'Toggle dark mode',
description: 'Toggle dark mode on or off.',
action: () => styleStore.toggleDark(),
icon: SunIcon,
category: 'Actions',
keywords: ['dark', 'theme', 'toggle', 'mode', 'light', 'system'],
},
{
name: 'Github repository',
href: 'https://github.com/CorentinTh/it-tools',
category: 'External',
description: 'View the source code of it-tools on Github.',
keywords: ['github', 'repo', 'repository', 'source', 'code'],
icon: GithubIcon,
},
{
name: 'Report a bug or an issue',
description: 'Report a bug or an issue to help improve it-tools.',
href: 'https://github.com/CorentinTh/it-tools/issues/new/choose',
category: 'Actions',
keywords: ['report', 'issue', 'bug', 'problem', 'error'],
icon: BugIcon,
},
{
name: 'About',
description: 'Learn more about IT-Tools.',
to: '/about',
category: 'Pages',
keywords: ['about', 'learn', 'more', 'info', 'information'],
icon: InfoIcon,
},
];
const { searchResult } = useFuzzySearch({
search: searchPrompt,
data: searchOptions,
options: {
keys: [{ name: 'name', weight: 2 }, 'description', 'keywords', 'category'],
threshold: 0.3,
},
});
const filteredSearchResult = computed(() =>
_.chain(searchResult.value).groupBy('category').mapValues(categoryOptions => _.take(categoryOptions, 5)).value());
return {
filteredSearchResult,
searchPrompt,
};
});

View File

@@ -0,0 +1,14 @@
import type { Component } from 'vue';
import type { RouteLocationRaw } from 'vue-router';
export interface PaletteOption {
name: string
description?: string
icon?: Component
action?: () => void
to?: RouteLocationRaw
category: string
keywords?: string[]
href?: string
closeOnSelect?: boolean
}

View File

@@ -0,0 +1,154 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import _ from 'lodash';
import { useCommandPaletteStore } from './command-palette.store';
import type { PaletteOption } from './command-palette.types';
const isModalOpen = ref(false);
const inputRef = ref();
const router = useRouter();
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
const commandPaletteStore = useCommandPaletteStore();
const { searchPrompt, filteredSearchResult } = storeToRefs(commandPaletteStore);
const keys = useMagicKeys({
passive: false,
onEventFired(e) {
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') {
e.preventDefault();
}
if (e.metaKey && e.key === 'k' && e.type === 'keydown') {
e.preventDefault();
}
},
});
whenever(isModalOpen, () => inputRef.value?.focus());
whenever(keys.ctrl_k, open);
whenever(keys.meta_k, open);
whenever(keys.escape, close);
function open() {
return isModalOpen.value = true;
}
function close() {
isModalOpen.value = false;
searchPrompt.value = '';
}
const selectedOptionIndex = ref(0);
function handleKeydown(event: KeyboardEvent) {
const { key } = event;
const isEnterPressed = key === 'Enter';
const isArrowUpOrDown = ['ArrowUp', 'ArrowDown'].includes(key);
const isArrowDown = key === 'ArrowDown';
if (isArrowUpOrDown) {
const increment = isArrowDown ? 1 : -1;
const maxIndex = Math.max(_.chain(filteredSearchResult.value).values().flatten().size().value() - 1, 0);
selectedOptionIndex.value = Math.min(Math.max(selectedOptionIndex.value + increment, 0), maxIndex);
return;
}
if (isEnterPressed) {
const option = _.chain(filteredSearchResult.value)
.values()
.flatten()
.nth(selectedOptionIndex.value)
.value();
activateOption(option);
}
}
function getOptionIndex(option: PaletteOption) {
return _.chain(filteredSearchResult.value)
.values()
.flatten()
.findIndex(o => o === option)
.value();
}
function activateOption(option: PaletteOption) {
const { closeOnSelect } = option;
if (option.action) {
option.action();
if (closeOnSelect) {
close();
}
return;
}
const closeAfterNavigation = closeOnSelect || _.isUndefined(closeOnSelect);
if (option.to) {
router.push(option.to);
if (closeAfterNavigation) {
close();
}
return;
}
if (option.href) {
window.open(option.href, '_blank');
if (closeAfterNavigation) {
close();
}
}
}
</script>
<template>
<div flex-1>
<c-button w-full important:justify-start @click="isModalOpen = true">
<span flex items-center gap-3 op-40>
<icon-mdi-search />
{{ $t('search.label') }}
<span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline>
{{ isMac ? 'Cmd' : 'Ctrl' }}&nbsp;+&nbsp;K
</span>
</span>
</c-button>
<c-modal v-model:open="isModalOpen" class="palette-modal" shadow-xl important:max-w-650px important:pa-12px @keydown="handleKeydown">
<c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable />
<div v-for="(options, category) in filteredSearchResult" :key="category">
<div ml-3 mt-3 text-sm font-bold text-primary op-60>
{{ category }}
</div>
<command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" />
</div>
</c-modal>
</div>
</template>
<style scoped lang="less">
.c-input-text {
font-size: 18px;
::v-deep(.input-wrapper) {
padding: 4px;
padding-left: 18px;
}
}
.c-modal--overlay {
align-items: flex-start !important;
padding-top: 80px;
}
</style>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { PaletteOption } from '../command-palette.types';
const props = withDefaults(defineProps<{ option: PaletteOption; selected?: boolean }>(), {
selected: false,
});
const emit = defineEmits(['activated']);
const { option } = toRefs(props);
const { selected } = toRefs(props);
</script>
<template>
<div
role="option"
:aria-selected="selected"
:class="{
'text-white': selected,
'bg-primary': selected,
}"
w-full flex cursor-pointer items-center overflow-hidden rounded pa-3 transition hover:bg-primary hover:text-white
@click="() => emit('activated', option)"
>
<component :is="option.icon" v-if="option.icon" mr-3 h-30px w-30px shrink-0 op-50 />
<div flex-1 overflow-hidden>
<div truncate font-bold lh-tight op-90>
{{ option.name }}
</div>
<div v-if="option.description" truncate lh-tight op-60>
{{ option.description }}
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
const { availableLocales, locale } = useI18n();
const localesLong: Record<string, string> = {
en: 'English',
es: 'Español',
fr: 'Français',
pt: 'Português',
ru: 'Русский',
zh: '中文',
};
const localeOptions = computed(() =>
availableLocales.map(locale => ({
label: localesLong[locale] ?? locale,
value: locale,
})),
);
</script>
<template>
<c-select
v-model:value="locale"
:options="localeOptions"
placeholder="Select a language"
w-100px
/>
</template>

View File

@@ -0,0 +1,7 @@
import { format } from 'date-fns';
export { getUrlFriendlyDateTime };
function getUrlFriendlyDateTime({ date = new Date() }: { date?: Date } = {}) {
return format(date, 'yyyy-MM-dd-HH-mm-ss');
}

View File

@@ -0,0 +1,5 @@
function clamp({ value, min = 0, max = 100 }: { value: number; min?: number; max?: number }) {
return Math.min(Math.max(value, min), max);
}
export { clamp };

View File

@@ -16,7 +16,7 @@ function useTracker() {
const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible');
if (_.isNil(plausible)) {
throw new Error('Plausible must be instantiated');
throw new TypeError('Plausible must be instantiated');
}
const tracker = createTrackerService({ plausible });

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { Coffee } from '@vicons/tabler';
import { useHead } from '@vueuse/head';
useHead({ title: 'Page not found - IT Tools' });
@@ -7,14 +6,22 @@ useHead({ title: 'Page not found - IT Tools' });
<template>
<div mt-20 flex flex-col items-center>
<n-icon :component="Coffee" size="100" depth="3" />
<span text-90px lh-1 op-50>
<icon-mdi:kettle-steam-outline />
</span>
<n-h1 m-0 mt-3>404 Not Found</n-h1>
<n-text mt-4 block depth="3">Sorry, this page does not seem to exist</n-text>
<n-text mb-8 block depth="3">Maybe the cache is doing tricky things, try force-refreshing?</n-text>
<h1 m-0 mt-3>
{{ $t('404.notFound') }}
</h1>
<div mt-4 op-60>
{{ $t('404.sorry') }}
</div>
<div mb-8 op-60>
{{ $t('404.maybe') }}
</div>
<router-link to="/" #="{ navigate, href }" custom>
<n-button tag="a" :href="href" secondary @click="navigate"> Back home </n-button>
</router-link>
<c-button to="/">
{{ $t('404.backHome') }}
</c-button>
</div>
</template>

View File

@@ -1,100 +1,9 @@
<script setup lang="ts">
import { useTracker } from '@/modules/tracker/tracker.services';
import { useHead } from '@vueuse/head';
useHead({ title: 'About - IT Tools' });
const { tracker } = useTracker();
</script>
<template>
<div class="about-page">
<n-h1>About</n-h1>
<n-p>
This wonderful website, made with by
<n-button text tag="a" href="https://github.com/CorentinTh" target="_blank" rel="noopener" type="primary">
Corentin Thomasset </n-button
>, aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to
share it to people you think may find it useful too and don't forget to pin it in your shortcut bar !
</n-p>
<n-p>
IT Tools is open-source (under the MIT license) and free, and will always be, but it cost me money to host and
renew the domain name, if you want to support my work, and encourage me to add more tools, please consider
supporting by
<n-button
type="primary"
tag="a"
text
href="https://www.buymeacoffee.com/cthmsst"
rel="noopener"
target="_blank"
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
>
sponsoring me </n-button
>.
</n-p>
<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 open-source libraries are used in some tools, you may find the complete list in the
<n-button
type="primary"
tag="a"
text
href="https://github.com/CorentinTh/it-tools/blob/main/package.json"
rel="noopener"
target="_blank"
>
package.json
</n-button>
file of the repository.
</n-p>
<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
<n-button
type="primary"
tag="a"
text
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=enhancement&template=feature_request.md&title=%5BFEAT%5D%20My%20feature"
rel="noopener"
target="_blank"
>
issues section
</n-button>
in the GitHub repository.
</n-p>
<n-p>
And if you found a bug, or something broken that doesn't work as expected, please fill a bug report in the
<n-button
type="primary"
tag="a"
text
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=bug&template=bug_report.md&title=%5BBUG%5D%20My%20bug"
rel="noopener"
target="_blank"
>
issues section
</n-button>
in the GitHub repository.
</n-p>
</div>
<c-markdown :markdown="$t('about.content')" mx-auto mt-50px max-w-600px />
</template>
<style scoped lang="less">
.about-page {
max-width: 600px;
margin: 50px auto;
box-sizing: border-box;
.n-h2 {
margin-bottom: 0px;
}
.n-p {
text-align: justify;
}
}
</style>

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import { config } from '@/config';
import { useToolStore } from '@/tools/tools.store';
import { Heart } from '@vicons/tabler';
import { useHead } from '@vueuse/head';
import ColoredCard from '../components/ColoredCard.vue';
import ToolCard from '../components/ToolCard.vue';
import { useToolStore } from '@/tools/tools.store';
import { config } from '@/config';
const toolStore = useToolStore();
useHead({ title: 'IT Tools - Handy online tools for developers' });
const { t } = useI18n();
</script>
<template>
@@ -16,53 +17,52 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
<div class="grid-wrapper">
<n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
<n-gi>
<colored-card title="You like it-tools?" :icon="Heart">
Give us a star on
<ColoredCard :title="$t('home.follow.title')" :icon="Heart">
{{ $t('home.follow.p1') }}
<a
href="https://github.com/CorentinTh/it-tools"
rel="noopener"
target="_blank"
aria-label="IT-Tools' GitHub repository"
>GitHub</a
>
or follow us on
:aria-label="$t('home.follow.githubRepository')"
>GitHub</a>
{{ $t('home.follow.p2') }}
<a
href="https://twitter.com/ittoolsdottech"
rel="noopener"
target="_blank"
aria-label="IT-Tools' Twitter account"
>Twitter</a
>! Thank you
:aria-label="$t('home.follow.twitterAccount')"
>Twitter</a>.
{{ $t('home.follow.thankYou') }}
<n-icon :component="Heart" />
</colored-card>
</ColoredCard>
</n-gi>
</n-grid>
<transition name="height">
<div v-if="toolStore.favoriteTools.length > 0">
<n-h3>Your favorite tools</n-h3>
<n-h3>{{ $t('home.categories.favoriteTools') }}</n-h3>
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
<n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name">
<tool-card :tool="tool" />
<ToolCard :tool="tool" />
</n-gi>
</n-grid>
</div>
</transition>
<div v-if="toolStore.newTools.length > 0">
<n-h3>Newest tools</n-h3>
<n-h3>{{ t('home.categories.newestTools') }}</n-h3>
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
<n-gi v-for="tool in toolStore.newTools" :key="tool.name">
<tool-card :tool="tool" />
<ToolCard :tool="tool" />
</n-gi>
</n-grid>
</div>
<n-h3>All the tools</n-h3>
<n-h3>{{ $t('home.categories.allTools') }}</n-h3>
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
<n-gi v-for="tool in toolStore.tools" :key="tool.name">
<transition>
<tool-card :tool="tool" />
<ToolCard :tool="tool" />
</transition>
</n-gi>
</n-grid>

View File

@@ -0,0 +1,37 @@
import type { Plugin } from 'vue';
import { createI18n } from 'vue-i18n';
import baseMessages from '@intlify/unplugin-vue-i18n/messages';
import _ from 'lodash';
import { parse as parseYaml } from 'yaml';
const i18nFiles = import.meta.glob('../tools/*/locales/**.yml', { as: 'raw' });
const messagesByTools = await Promise.all(_.map(i18nFiles, async (fileDescriptor, path) => {
const [, locale] = path.match(/\.\/tools\/.*?\/locales\/(.*)\.ya?ml$/i) ?? [];
const content = parseYaml(await fileDescriptor());
return { [locale]: content };
}));
const messages = _.merge(
baseMessages,
_.merge({}, ...messagesByTools),
);
const i18n = createI18n({
legacy: false,
locale: 'en',
messages,
});
export const i18nPlugin: Plugin = {
install: (app) => {
app.use(i18n);
},
};
export const translate = function (localeKey: string) {
// @ts-expect-error global
const hasKey = i18n.global.te(localeKey, i18n.global.locale);
return hasKey ? i18n.global.t(localeKey) : localeKey;
};

View File

@@ -1,8 +1,8 @@
import { config } from '@/config';
import { noop } from 'lodash';
import Plausible from 'plausible-tracker';
import type { App } from 'vue';
import { config } from '@/config';
function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> {
return {
@@ -15,11 +15,11 @@ function createPlausibleInstance({
config,
}: {
config: {
isTrackerEnabled: boolean;
domain: string;
apiHost: string;
trackLocalhost: boolean;
};
isTrackerEnabled: boolean
domain: string
apiHost: string
trackLocalhost: boolean
}
}) {
if (config.isTrackerEnabled) {
return Plausible(config);

View File

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

33
src/shims.d.ts vendored
View File

@@ -1,10 +1,41 @@
declare module '*.vue' {
import type { ComponentOptions, ComponentOptions } from 'vue';
import type { ComponentOptions } from 'vue';
const Component: ComponentOptions;
export default Component;
}
declare module '*.md' {
import type { ComponentOptions } from 'vue';
const Component: ComponentOptions;
export default Component;
}
declare module 'iarna-toml-esm' {
export const parse: (toml: string) => any;
export const stringify: (obj: any) => string;
}
declare module 'emojilib' {
const lib: Record<string, string[]>;
export default lib;
}
declare module 'unicode-emoji-json' {
const emoji: Record<string, {
name: string;
slug: string;
group: string;
emoji_version: string;
unicode_version: string;
skin_tone_support: boolean;
skin_tone_support_unicode_version: string;
}>;
export default emoji;
}
declare module 'pdf-signature-reader' {
const verifySignature: (pdf: ArrayBuffer) => ({signatures: SignatureInfo[]});
export default verifySignature;
}

View File

@@ -1,17 +1,19 @@
import { useMediaQuery, useStorage } from '@vueuse/core';
import { useDark, useMediaQuery, useStorage, useToggle } from '@vueuse/core';
import { defineStore } from 'pinia';
import { watch, type Ref } from 'vue';
import { type Ref, watch } from 'vue';
export const useStyleStore = defineStore('style', {
state: () => {
const isDarkTheme = useStorage('isDarkTheme', true) as Ref<boolean>;
const isDarkTheme = useDark();
const toggleDark = useToggle(isDarkTheme);
const isSmallScreen = useMediaQuery('(max-width: 700px)');
const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>;
watch(isSmallScreen, (v) => (isMenuCollapsed.value = v));
watch(isSmallScreen, v => (isMenuCollapsed.value = v));
return {
isDarkTheme,
toggleDark,
isMenuCollapsed,
isSmallScreen,
};

View File

@@ -1,45 +1,10 @@
<template>
<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 :disabled="base64Input === '' || !base64InputValidation.isValid" secondary @click="downloadFile()">
Download file
</n-button>
</n-space>
</n-card>
<n-card title="File to base64">
<n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image">
<n-upload-dragger>
<div mb-2>
<n-icon size="35" :depth="3" :component="Upload" />
</div>
<n-text style="font-size: 14px"> Click or drag a file to this area to upload </n-text>
</n-upload-dragger>
</n-upload>
<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>
</n-card>
</template>
<script setup lang="ts">
import { useBase64 } from '@vueuse/core';
import type { Ref } from 'vue';
import { useCopy } from '@/composable/copy';
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
import { useValidation } from '@/composable/validation';
import { isValidBase64 } from '@/utils/base64';
import { Upload } from '@vicons/tabler';
import { useBase64 } from '@vueuse/core';
import type { UploadFileInfo } from 'naive-ui';
import { ref, type Ref } from 'vue';
const base64Input = ref('');
const { download } = useDownloadFileFromBase64({ source: base64Input });
@@ -48,40 +13,66 @@ const base64InputValidation = useValidation({
rules: [
{
message: 'Invalid base 64 string',
validator: (value) => isValidBase64(value.trim()),
validator: value => isValidBase64(value.trim()),
},
],
});
function downloadFile() {
if (!base64InputValidation.isValid) return;
if (!base64InputValidation.isValid) {
return;
}
try {
download();
} catch (_) {
}
catch (_) {
//
}
}
const fileList = ref();
const fileInput = ref() as Ref<File>;
const { base64: fileBase64 } = useBase64(fileInput);
const { copy: copyFileBase64 } = useCopy({ source: fileBase64, text: 'Base64 string copied to the clipboard' });
async function onUpload({ file: { file } }: { file: UploadFileInfo }) {
async function onUpload(file: File) {
if (file) {
fileList.value = [];
fileInput.value = file;
}
}
</script>
<style lang="less" scoped>
.n-input,
.n-upload {
margin-bottom: 15px;
}
<template>
<c-card title="Base64 to file">
<c-input-text
v-model:value="base64Input"
multiline
placeholder="Put your base64 file string here..."
rows="5"
:validation="base64InputValidation"
mb-2
/>
<div flex justify-center>
<c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()">
Download file
</c-button>
</div>
</c-card>
<c-card title="File to base64">
<c-file-upload title="Drag and drop a file here, or click to select a file" @file-upload="onUpload" />
<c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" my-2 />
<div flex justify-center>
<c-button @click="copyFileBase64()">
Copy
</c-button>
</div>
</c-card>
</template>
<style lang="less" scoped>
::v-deep(.n-upload-trigger) {
width: 100%;
}

View File

@@ -4,7 +4,7 @@ import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Base64 file converter',
path: '/base64-file-converter',
description: "Convert string, files or images into a it's base64 representation.",
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-file-converter.vue'),
icon: FileDigit,

View File

@@ -1,55 +1,90 @@
<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 encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false);
const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false);
const textInput = ref('');
const base64Output = computed(() => textToBase64(textInput.value));
const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.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 textOutput = computed(() =>
withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''),
);
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()) }],
});
const b64ValidationRules = [
{
message: 'Invalid base64 string',
validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }),
},
];
const b64ValidationWatch = [decodeUrlSafe];
</script>
<template>
<c-card title="String to base64">
<n-form-item label="Encode URL safe" label-placement="left">
<n-switch v-model:value="encodeUrlSafe" />
</n-form-item>
<c-input-text
v-model:value="textInput"
multiline
placeholder="Put your string here..."
rows="5"
label="String to encode"
raw-text
mb-5
/>
<c-input-text
label="Base64 of string"
:value="base64Output"
multiline
readonly
placeholder="The base64 encoding of your string will be here"
rows="5"
mb-5
/>
<div flex justify-center>
<c-button @click="copyTextBase64()">
Copy base64
</c-button>
</div>
</c-card>
<c-card title="Base64 to string">
<n-form-item label="Decode URL safe" label-placement="left">
<n-switch v-model:value="decodeUrlSafe" />
</n-form-item>
<c-input-text
v-model:value="base64Input"
multiline
placeholder="Your base64 string..."
rows="5"
:validation-rules="b64ValidationRules"
:validation-watch="b64ValidationWatch"
label="Base64 string to decode"
mb-5
/>
<c-input-text
v-model:value="textOutput"
label="Decoded string"
placeholder="The decoded string will be here"
multiline
rows="5"
readonly
mb-5
/>
<div flex justify-center>
<c-button @click="copyText()">
Copy decoded string
</c-button>
</div>
</c-card>
</template>

View File

@@ -1,37 +1,6 @@
<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('');
@@ -40,6 +9,34 @@ const header = computed(() => `Authorization: Basic ${textToBase64(`${username.v
const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' });
</script>
<template>
<div>
<c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 />
<c-input-text
v-model:value="password"
label="Password"
placeholder="Your password..."
clearable
raw-text
mb-2
type="password"
/>
<c-card>
<n-statistic label="Authorization header:" class="header">
<n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none">
{{ header }}
</n-scrollbar>
</n-statistic>
</c-card>
<div mt-5 flex justify-center>
<c-button @click="copy()">
Copy header
</c-button>
</div>
</div>
</template>
<style lang="less" scoped>
::v-deep(.n-statistic-value__content) {
font-family: monospace;

View File

@@ -1,63 +1,7 @@
<template>
<n-card title="Hash">
<n-form label-width="120">
<n-form-item label="Your string: " label-placement="left">
<n-input
v-model:value="input"
placeholder="Your string to bcrypt..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</n-form-item>
<n-form-item label="Salt count: " label-placement="left">
<n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
</n-form-item>
<n-input :value="hashed" readonly style="text-align: center" />
</n-form>
<br />
<n-space justify="center">
<n-button secondary @click="copy"> Copy hash </n-button>
</n-space>
</n-card>
<n-card title="Compare string with hash">
<n-form label-width="120">
<n-form-item label="Your string: " label-placement="left">
<n-input
v-model:value="compareString"
placeholder="Your string to compare..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</n-form-item>
<n-form-item label="Your hash: " label-placement="left">
<n-input
v-model:value="compareHash"
placeholder="Your hahs to compare..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</n-form-item>
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
<div class="compare-result" :class="{ positive: compareMatch }">
{{ compareMatch ? 'Yes' : 'No' }}
</div>
</n-form-item>
</n-form>
</n-card>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { hashSync, compareSync } from 'bcryptjs';
import { useCopy } from '@/composable/copy';
import { compareSync, hashSync } from 'bcryptjs';
import { useThemeVars } from 'naive-ui';
import { useCopy } from '@/composable/copy';
const themeVars = useThemeVars();
@@ -71,6 +15,48 @@ const compareHash = ref('');
const compareMatch = computed(() => compareSync(compareString.value, compareHash.value));
</script>
<template>
<c-card title="Hash">
<c-input-text
v-model:value="input"
placeholder="Your string to bcrypt..."
raw-text
label="Your string: "
label-position="left"
label-align="right"
label-width="120px"
mb-2
/>
<n-form-item label="Salt count: " label-placement="left" label-width="120">
<n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
</n-form-item>
<c-input-text :value="hashed" readonly text-center />
<div mt-5 flex justify-center>
<c-button @click="copy()">
Copy hash
</c-button>
</div>
</c-card>
<c-card title="Compare string with hash">
<n-form label-width="120">
<n-form-item label="Your string: " label-placement="left">
<c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text />
</n-form-item>
<n-form-item label="Your hash: " label-placement="left">
<c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text />
</n-form-item>
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
<div class="compare-result" :class="{ positive: compareMatch }">
{{ compareMatch ? 'Yes' : 'No' }}
</div>
</n-form-item>
</n-form>
</c-card>
</template>
<style lang="less" scoped>
.compare-result {
color: v-bind('themeVars.errorColor');

View File

@@ -13,22 +13,22 @@ function computeAverage({ data }: { data: number[] }) {
function computeVariance({ data }: { data: number[] }) {
const mean = computeAverage({ data });
const squaredDiffs = data.map((value) => Math.pow(value - mean, 2));
const squaredDiffs = data.map(value => (value - mean) ** 2);
return computeAverage({ data: squaredDiffs });
}
function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; headerMap?: Record<string, string> }) {
function arrayToMarkdownTable({ data, headerMap = {} }: { data: Record<string, unknown>[]; headerMap?: Record<string, string> }) {
if (!Array.isArray(data) || data.length === 0) {
return '';
}
const headers = Object.keys(data[0]);
const rows = data.map((obj) => Object.values(obj));
const rows = data.map(obj => Object.values(obj));
const headerRow = `| ${headers.map((header) => headerMap[header] ?? header).join(' | ')} |`;
const headerRow = `| ${headers.map(header => headerMap[header] ?? header).join(' | ')} |`;
const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`;
const dataRows = rows.map((row) => `| ${row.join(' | ')} |`).join('\n');
const dataRows = rows.map(row => `| ${row.join(' | ')} |`).join('\n');
return `${headerRow}\n${separatorRow}\n${dataRows}`;
}

View File

@@ -1,92 +1,11 @@
<template>
<n-scrollbar style="flex: 1" x-scrollable>
<n-space :wrap="false" style="flex: 1" justify="center" :size="0">
<div v-for="(suite, index) of suites" :key="index">
<n-card style="width: 292px; margin: 0 8px 5px">
<n-form-item label="Suite name:" :show-feedback="false" label-placement="left">
<n-input v-model:value="suite.title" placeholder="Suite name..." />
</n-form-item>
<n-divider></n-divider>
<n-form-item label="Suite values" :show-feedback="false">
<dynamic-values v-model:values="suite.data" />
</n-form-item>
</n-card>
<n-space justify="center">
<n-button v-if="suites.length > 1" quaternary @click="suites.splice(index, 1)">
<template #icon>
<n-icon :component="Trash" depth="3" />
</template>
Delete suite
</n-button>
<n-button quaternary @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })">
<template #icon>
<n-icon :component="Plus" depth="3" />
</template>
Add suite
</n-button>
</n-space>
</div>
</n-space>
<br />
</n-scrollbar>
<div style="flex: 0 0 100%">
<div style="max-width: 600px; margin: 0 auto">
<n-space justify="center">
<n-form-item label="Unit:" label-placement="left">
<n-input v-model:value="unit" placeholder="Unit (eg: ms)" />
</n-form-item>
<n-button
tertiary
@click="
suites = [
{ title: 'Suite 1', data: [] },
{ title: 'Suite 2', data: [] },
]
"
>Reset suites</n-button
>
</n-space>
<n-table>
<thead>
<tr>
<th>{{ header.position }}</th>
<th>{{ header.title }}</th>
<th>{{ header.size }}</th>
<th>{{ header.mean }}</th>
<th>{{ header.variance }}</th>
</tr>
</thead>
<tbody>
<tr v-for="{ title, size, mean, variance, position } of results" :key="title">
<td>{{ position }}</td>
<td>{{ title }}</td>
<td>{{ size }}</td>
<td>{{ mean }}</td>
<td>{{ variance }}</td>
</tr>
</tbody>
</n-table>
<br />
<n-space justify="center">
<n-button tertiary @click="copyAsMarkdown">Copy as markdown table</n-button>
<n-button tertiary @click="copyAsBulletList">Copy as bullet list</n-button>
</n-space>
</div>
</div>
</template>
<script setup lang="ts">
import { Trash, Plus } from '@vicons/tabler';
import { useClipboard, useStorage } from '@vueuse/core';
import { Plus, Trash } from '@vicons/tabler';
import { useStorage } from '@vueuse/core';
import _ from 'lodash';
import { computed } from 'vue';
import { computeAverage, computeVariance, arrayToMarkdownTable } from './benchmark-builder.models';
import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models';
import DynamicValues from './dynamic-values.vue';
import { useCopy } from '@/composable/copy';
const suites = useStorage('benchmark-builder:suites', [
{ title: 'Suite 1', data: [5, 10] },
@@ -116,8 +35,8 @@ const results = computed(() => {
const deltaWithBestMean = mean - bestMean;
const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean);
const comparisonValues: string =
index !== 0 && bestMean !== mean ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
const comparisonValues: string
= (index !== 0 && bestMean !== mean) ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
return {
position: index + 1,
@@ -129,14 +48,14 @@ const results = computed(() => {
});
});
const { copy } = useClipboard();
const { copy } = useCopy({ createToast: false });
const header = {
position: 'Position',
title: 'Suite',
size: 'Samples',
mean: 'Mean',
variance: 'Variance',
position: 'Position',
};
function copyAsMarkdown() {
@@ -159,4 +78,69 @@ function copyAsBulletList() {
}
</script>
<style lang="less" scoped></style>
<template>
<n-scrollbar style="flex: 1" x-scrollable>
<div mb-5 flex flex-1 flex-nowrap justify-center gap-12px>
<div v-for="(suite, index) of suites" :key="index">
<c-card style="width: 294px">
<c-input-text
v-model:value="suite.title"
label-position="left"
label="Suite name"
placeholder="Suite name..."
clearable
/>
<n-divider />
<n-form-item label="Suite values" :show-feedback="false">
<DynamicValues v-model:values="suite.data" />
</n-form-item>
</c-card>
<div flex justify-center>
<c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)">
<n-icon :component="Trash" depth="3" mr-2 size="18" />
Delete suite
</c-button>
<c-button
variant="text"
@click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })"
>
<n-icon :component="Plus" depth="3" mr-2 size="18" />
Add suite
</c-button>
</div>
</div>
</div>
</n-scrollbar>
<div style="flex: 0 0 100%">
<div style="max-width: 600px; margin: 0 auto">
<div mx-auto max-w-sm flex justify-center gap-3>
<c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 />
<c-button
@click="
suites = [
{ title: 'Suite 1', data: [] },
{ title: 'Suite 2', data: [] },
]
"
>
Reset suites
</c-button>
</div>
<c-table :data="results" :headers="header" />
<div mt-5 flex justify-center gap-3>
<c-button @click="copyAsMarkdown()">
Copy as markdown table
</c-button>
<c-button @click="copyAsBulletList()">
Copy as bullet list
</c-button>
</div>
</div>
</div>
</template>

View File

@@ -1,45 +1,15 @@
<template>
<div>
<n-space v-for="(value, index) of values" :key="index" :wrap="false" style="margin-bottom: 5px" :size="5">
<n-input-number
:ref="refs.set"
v-model:value="values[index]"
:show-button="false"
placeholder="Set your measure..."
autofocus
@keydown.enter="onInputEnter(index)"
/>
<n-tooltip>
<template #trigger>
<n-button circle quaternary @click="values.splice(index, 1)">
<template #icon>
<n-icon :component="Trash" depth="3" />
</template>
</n-button>
</template>
Delete value
</n-tooltip>
</n-space>
<n-button tertiary @click="addValue">
<template #icon>
<n-icon :component="Plus" />
</template>
Add a measure
</n-button>
</div>
</template>
<script setup lang="ts">
import { Trash, Plus } from '@vicons/tabler';
import { Plus, Trash } from '@vicons/tabler';
import { useTemplateRefsList, useVModel } from '@vueuse/core';
import { NInputNumber } from 'naive-ui';
import { nextTick } from 'vue';
const props = defineProps<{ values: (number | null)[] }>();
const emit = defineEmits(['update:values']);
const refs = useTemplateRefsList<typeof NInputNumber>();
const props = defineProps<{ values: (number | null)[] }>();
const emit = defineEmits(['update:values']);
const values = useVModel(props, 'values', emit);
async function addValue() {
@@ -58,4 +28,27 @@ function onInputEnter(index: number) {
}
</script>
<style scoped></style>
<template>
<div>
<div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2>
<NInputNumber
:ref="refs.set"
v-model:value="values[index]"
:show-button="false"
placeholder="Set your measure..."
autofocus
@keydown.enter="onInputEnter(index)"
/>
<c-tooltip tooltip="Delete this value">
<c-button circle variant="text" @click="values.splice(index, 1)">
<n-icon :component="Trash" depth="3" size="18" />
</c-button>
</c-tooltip>
</div>
<c-button @click="addValue">
<n-icon :component="Plus" depth="3" mr-2 size="18" />
Add a measure
</c-button>
</div>
</template>

View File

@@ -1,67 +1,4 @@
<template>
<div>
<n-card>
<n-grid cols="3" x-gap="12">
<n-gi span="1">
<n-form-item label="Language:">
<n-select
v-model:value="language"
:options="Object.keys(languages).map((label) => ({ label, value: label }))"
/>
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item
label="Entropy (seed):"
:feedback="entropyValidation.message"
:validation-status="entropyValidation.status"
>
<n-input-group>
<n-input v-model:value="entropy" placeholder="Your string..." />
<n-button @click="refreshEntropy">
<n-icon size="22">
<Refresh />
</n-icon>
</n-button>
<n-button @click="copyEntropy">
<n-icon size="22">
<Copy />
</n-icon>
</n-button>
</n-input-group>
</n-form-item>
</n-gi>
</n-grid>
<n-form-item
label="Passphrase (mnemonic):"
:feedback="mnemonicValidation.message"
:validation-status="mnemonicValidation.status"
>
<n-input-group>
<n-input
v-model:value="passphrase"
style="text-align: center; flex: 1"
placeholder="Your mnemonic..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
<n-button @click="copyPassphrase">
<n-icon size="22" :component="Copy" />
</n-button>
</n-input-group>
</n-form-item>
</n-card>
</div>
</template>
<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 {
chineseSimplifiedWordList,
chineseTraditionalWordList,
@@ -78,19 +15,23 @@ import {
spanishWordList,
} from '@it-tools/bip39';
import { Copy, Refresh } from '@vicons/tabler';
import { computed, ref } from 'vue';
import { useCopy } from '@/composable/copy';
import { useValidation } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean';
import { withDefaultOnError } from '@/utils/defaults';
const languages = {
English: englishWordList,
'English': englishWordList,
'Chinese simplified': chineseSimplifiedWordList,
'Chinese traditional': chineseTraditionalWordList,
Czech: czechWordList,
French: frenchWordList,
Italian: italianWordList,
Japanese: japaneseWordList,
Korean: koreanWordList,
Portuguese: portugueseWordList,
Spanish: spanishWordList,
'Czech': czechWordList,
'French': frenchWordList,
'Italian': italianWordList,
'Japanese': japaneseWordList,
'Korean': koreanWordList,
'Portuguese': portugueseWordList,
'Spanish': spanishWordList,
};
const entropy = ref(generateEntropy());
@@ -111,11 +52,11 @@ const entropyValidation = useValidation({
source: entropy,
rules: [
{
validator: (value) => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
validator: value => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4',
},
{
validator: (value) => /^[a-fA-F0-9]*$/.test(value),
validator: value => /^[a-fA-F0-9]*$/.test(value),
message: 'Entropy should be an hexadecimal string',
},
],
@@ -125,7 +66,7 @@ const mnemonicValidation = useValidation({
source: passphrase,
rules: [
{
validator: (value) => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
validator: value => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
message: 'Invalid mnemonic',
},
],
@@ -138,3 +79,53 @@ function refreshEntropy() {
const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' });
const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' });
</script>
<template>
<div>
<n-grid cols="3" x-gap="12">
<n-gi span="1">
<c-select
v-model:value="language"
searchable
label="Language:"
:options="Object.keys(languages)"
/>
</n-gi>
<n-gi span="2">
<n-form-item
label="Entropy (seed):"
:feedback="entropyValidation.message"
:validation-status="entropyValidation.status"
>
<n-input-group>
<c-input-text v-model:value="entropy" placeholder="Your string..." />
<c-button @click="refreshEntropy()">
<n-icon size="22">
<Refresh />
</n-icon>
</c-button>
<c-button @click="copyEntropy()">
<n-icon size="22">
<Copy />
</n-icon>
</c-button>
</n-input-group>
</n-form-item>
</n-gi>
</n-grid>
<n-form-item
label="Passphrase (mnemonic):"
:feedback="mnemonicValidation.message"
:validation-status="mnemonicValidation.status"
>
<n-input-group>
<c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text />
<c-button @click="copyPassphrase()">
<n-icon size="22" :component="Copy" />
</c-button>
</n-input-group>
</n-form-item>
</div>
</template>

View File

@@ -0,0 +1,217 @@
<script setup lang="ts">
import _ from 'lodash';
import { useMediaRecorder } from './useMediaRecorder';
interface Media { type: 'image' | 'video'; value: string; createdAt: Date }
const {
videoInputs: cameras,
audioInputs: microphones,
permissionGranted,
isSupported,
ensurePermissions,
} = useDevicesList({
requestPermissions: true,
constraints: { video: true, audio: true },
onUpdated() {
refreshCurrentDevices();
},
});
const video = ref<HTMLVideoElement>();
const medias = ref<Media[]>([]);
const currentCamera = ref(cameras.value[0]?.deviceId);
const currentMicrophone = ref(microphones.value[0]?.deviceId);
const permissionCannotBePrompted = ref(false);
const {
stream,
start,
stop,
enabled: isMediaStreamAvailable,
} = useUserMedia({
constraints: computed(() => ({
video: { deviceId: currentCamera.value },
...(currentMicrophone.value ? { audio: { deviceId: currentMicrophone.value } } : {}),
})),
autoSwitch: true,
});
const {
isRecordingSupported,
onRecordAvailable,
startRecording,
stopRecording,
pauseRecording,
recordingState,
resumeRecording,
} = useMediaRecorder({
stream,
});
onRecordAvailable((value) => {
medias.value.unshift({ type: 'video', value, createdAt: new Date() });
});
function refreshCurrentDevices() {
if (_.isNil(currentCamera) || !cameras.value.find(i => i.deviceId === currentCamera.value)) {
currentCamera.value = cameras.value[0]?.deviceId;
}
if (_.isNil(microphones) || !microphones.value.find(i => i.deviceId === currentMicrophone.value)) {
currentMicrophone.value = microphones.value[0]?.deviceId;
}
}
function takeScreenshot() {
if (!video.value) {
return;
}
const canvas = document.createElement('canvas');
canvas.width = video.value.videoWidth;
canvas.height = video.value.videoHeight;
canvas.getContext('2d')?.drawImage(video.value, 0, 0);
const image = canvas.toDataURL('image/png');
medias.value.unshift({ type: 'image', value: image, createdAt: new Date() });
}
watchEffect(() => {
if (video.value && stream.value) {
video.value.srcObject = stream.value;
}
});
onBeforeUnmount(() => stop());
async function requestPermissions() {
try {
await ensurePermissions();
}
catch (e) {
permissionCannotBePrompted.value = true;
}
}
function downloadMedia({ type, value, createdAt }: Media) {
const link = document.createElement('a');
link.href = value;
link.download = `${type}-${createdAt.getTime()}.${type === 'image' ? 'png' : 'webm'}`;
link.click();
}
</script>
<template>
<div>
<c-card v-if="!isSupported">
Your browser does not support recording video from camera
</c-card>
<c-card v-else-if="!permissionGranted" text-center>
You need to grant permission to use your camera and microphone
<c-alert v-if="permissionCannotBePrompted" mt-4 text-left>
Your browser has blocked permission request or does not support it. You need to grant permission manually in
your browser settings (usually the lock icon in the address bar).
</c-alert>
<div v-else mt-4 flex justify-center>
<c-button @click="requestPermissions">
Grant permission
</c-button>
</div>
</c-card>
<c-card v-else>
<div flex flex-col gap-2>
<c-select
v-model:value="currentCamera"
label-position="left"
label-width="60px"
label="Video:"
:options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))"
placeholder="Select camera"
/>
<c-select
v-if="currentMicrophone && microphones.length > 0"
v-model:value="currentMicrophone"
label="Audio:"
label-position="left"
label-width="60px"
:options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))"
placeholder="Select microphone"
/>
</div>
<div v-if="!isMediaStreamAvailable" mt-3 flex justify-center>
<c-button type="primary" @click="start">
Start webcam
</c-button>
</div>
<div v-else>
<div my-2>
<video ref="video" autoplay controls playsinline max-h-full w-full />
</div>
<div flex items-center justify-between gap-2>
<c-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot">
<span mr-2> <icon-mdi-camera /></span>
Take screenshot
</c-button>
<div v-if="isRecordingSupported" flex justify-center gap-2>
<c-button v-if="recordingState === 'stopped'" @click="startRecording">
<span mr-2> <icon-mdi-video /></span>
Start recording
</c-button>
<c-button v-if="recordingState === 'recording'" @click="pauseRecording">
<span mr-2> <icon-mdi-pause /></span>
Pause
</c-button>
<c-button v-if="recordingState === 'paused'" @click="resumeRecording">
<span mr-2> <icon-mdi-play /></span>
Resume
</c-button>
<c-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording">
<span mr-2> <icon-mdi-record /></span>
Stop
</c-button>
</div>
<div v-else italic op-60>
Video recording is not supported in your browser
</div>
</div>
</div>
</c-card>
<div grid grid-cols-2 mt-5 gap-2>
<c-card v-for="({ type, value, createdAt }, index) in medias" :key="index">
<img v-if="type === 'image'" :src="value" max-h-full w-full alt="screenshot">
<video v-else :src="value" controls max-h-full w-full />
<div flex items-center justify-between>
<div font-bold>
{{ type === 'image' ? 'Screenshot' : 'Video' }}
</div>
<div flex gap-2>
<c-button @click="downloadMedia({ type, value, createdAt })">
<icon-mdi-download />
</c-button>
<c-button @click="medias = medias.filter((_ignored, i) => i !== index)">
<icon-mdi-delete-outline />
</c-button>
</div>
</div>
</c-card>
</div>
</div>
</template>

View File

@@ -0,0 +1,12 @@
import { Camera } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Camera recorder',
path: '/camera-recorder',
description: 'Take a picture or record a video from your webcam or camera.',
keywords: ['camera', 'recoder'],
component: () => import('./camera-recorder.vue'),
icon: Camera,
createdAt: new Date('2023-05-15'),
});

View File

@@ -0,0 +1,114 @@
import { type Ref, computed, ref } from 'vue';
export { useMediaRecorder };
function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): {
isRecordingSupported: Ref<boolean>
recordingState: Ref<'stopped' | 'recording' | 'paused'>
startRecording: () => void
stopRecording: () => void
pauseRecording: () => void
resumeRecording: () => void
onRecordAvailable: (cb: (url: string) => void) => void
} {
const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm'));
const mediaRecorder = ref<MediaRecorder | null>(null);
const recordedChunks = ref<Blob[]>([]);
const recordAvailable = createEventHook();
const recordingState = ref<'stopped' | 'recording' | 'paused'>('stopped');
const createVideo = () => {
const blob = new Blob(recordedChunks.value, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
recordedChunks.value = [];
return url;
};
const startRecording = () => {
if (!isRecordingSupported.value) {
return;
}
if (!stream.value) {
return;
}
if (recordingState.value !== 'stopped') {
return;
}
mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' });
mediaRecorder.value.ondataavailable = (e) => {
if (e.data.size > 0) {
recordedChunks.value.push(e.data);
}
};
mediaRecorder.value.onstop = () => {
recordAvailable.trigger(createVideo());
};
if (mediaRecorder.value.state !== 'inactive') {
return;
}
mediaRecorder.value.start();
recordingState.value = 'recording';
};
const stopRecording = () => {
if (!isRecordingSupported.value) {
return;
}
if (!mediaRecorder.value) {
return;
}
if (recordingState.value === 'stopped') {
return;
}
mediaRecorder.value.stop();
recordingState.value = 'stopped';
};
const pauseRecording = () => {
if (!isRecordingSupported.value) {
return;
}
if (!mediaRecorder.value) {
return;
}
if (recordingState.value !== 'recording') {
return;
}
mediaRecorder.value.pause();
recordingState.value = 'paused';
};
const resumeRecording = () => {
if (!isRecordingSupported.value) {
return;
}
if (!mediaRecorder.value) {
return;
}
if (recordingState.value !== 'paused') {
return;
}
mediaRecorder.value.resume();
recordingState.value = 'recording';
};
return {
isRecordingSupported,
startRecording,
stopRecording,
pauseRecording,
resumeRecording,
recordingState,
onRecordAvailable: recordAvailable.on,
};
}

View File

@@ -1,51 +1,4 @@
<template>
<n-card>
<n-form label-width="120" label-placement="left" :show-feedback="false">
<n-form-item label="Your string:">
<n-input v-model:value="input" />
</n-form-item>
<n-divider />
<n-form-item label="Camelcase:">
<input-copyable :value="camelCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Capitalcase:">
<input-copyable :value="capitalCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Constantcase:">
<input-copyable :value="constantCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Dotcase:">
<input-copyable :value="dotCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Headercase:">
<input-copyable :value="headerCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Nocase:">
<input-copyable :value="noCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Paramcase:">
<input-copyable :value="paramCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Pascalcase:">
<input-copyable :value="pascalCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Pathcase:">
<input-copyable :value="pathCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Sentencecase:">
<input-copyable :value="sentenceCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Snakecase:">
<input-copyable :value="snakeCase(input, baseConfig)" />
</n-form-item>
</n-form>
</n-card>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import {
camelCase,
capitalCase,
@@ -66,10 +19,95 @@ const baseConfig = {
};
const input = ref('lorem ipsum dolor sit amet');
const formats = computed(() => [
{
label: 'Lowercase:',
value: input.value.toLocaleLowerCase(),
},
{
label: 'Uppercase:',
value: input.value.toLocaleUpperCase(),
},
{
label: 'Camelcase:',
value: camelCase(input.value, baseConfig),
},
{
label: 'Capitalcase:',
value: capitalCase(input.value, baseConfig),
},
{
label: 'Constantcase:',
value: constantCase(input.value, baseConfig),
},
{
label: 'Dotcase:',
value: dotCase(input.value, baseConfig),
},
{
label: 'Headercase:',
value: headerCase(input.value, baseConfig),
},
{
label: 'Nocase:',
value: noCase(input.value, baseConfig),
},
{
label: 'Paramcase:',
value: paramCase(input.value, baseConfig),
},
{
label: 'Pascalcase:',
value: pascalCase(input.value, baseConfig),
},
{
label: 'Pathcase:',
value: pathCase(input.value, baseConfig),
},
{
label: 'Sentencecase:',
value: sentenceCase(input.value, baseConfig),
},
{
label: 'Snakecase:',
value: snakeCase(input.value, baseConfig),
},
{
label: 'Mockingcase:',
value: input.value
.split('')
.map((char, index) => (index % 2 === 0 ? char.toUpperCase() : char.toLowerCase()))
.join(''),
},
]);
const inputLabelAlignmentConfig = {
labelPosition: 'left',
labelWidth: '120px',
labelAlign: 'right',
};
</script>
<style lang="less" scoped>
.n-form-item {
margin: 5px 0;
}
</style>
<template>
<c-card>
<c-input-text
v-model:value="input"
label="Your string:"
placeholder="Your string..."
raw-text
v-bind="inputLabelAlignmentConfig"
/>
<div my-16px divider />
<InputCopyable
v-for="format in formats"
:key="format.label"
:value="format.value"
:label="format.label"
v-bind="inputLabelAlignmentConfig"
mb-1
/>
</c-card>
</template>

View File

@@ -1,5 +1,5 @@
import { expect, describe, it } from 'vitest';
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
import { describe, expect, it } from 'vitest';
import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service';
describe('chmod-calculator', () => {
describe('computeChmodOctalRepresentation', () => {
@@ -64,5 +64,67 @@ describe('chmod-calculator', () => {
}),
).to.eql('222');
});
it('get the symbolic representation from permissions', () => {
expect(
computeChmodSymbolicRepresentation({
permissions: {
owner: { read: true, write: true, execute: true },
group: { read: true, write: true, execute: true },
public: { read: true, write: true, execute: true },
},
}),
).to.eql('rwxrwxrwx');
expect(
computeChmodSymbolicRepresentation({
permissions: {
owner: { read: false, write: false, execute: false },
group: { read: false, write: false, execute: false },
public: { read: false, write: false, execute: false },
},
}),
).to.eql('---------');
expect(
computeChmodSymbolicRepresentation({
permissions: {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: true },
public: { read: true, write: false, execute: true },
},
}),
).to.eql('-w--wxr-x');
expect(
computeChmodSymbolicRepresentation({
permissions: {
owner: { read: true, write: false, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: false, execute: true },
},
}),
).to.eql('r---w---x');
expect(
computeChmodSymbolicRepresentation({
permissions: {
owner: { read: false, write: false, execute: true },
group: { read: false, write: true, execute: false },
public: { read: true, write: false, execute: false },
},
}),
).to.eql('--x-w-r--');
expect(
computeChmodSymbolicRepresentation({
permissions: {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
},
}),
).to.eql('-w--w--w-');
});
});
});

View File

@@ -1,7 +1,7 @@
import _ from 'lodash';
import type { GroupPermissions, Permissions } from './chmod-calculator.types';
export { computeChmodOctalRepresentation };
export { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation };
function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string {
const permissionValue = { read: 4, write: 2, execute: 1 };
@@ -15,3 +15,16 @@ function computeChmodOctalRepresentation({ permissions }: { permissions: Permiss
getGroupPermissionValue(permissions.public),
].join('');
}
function computeChmodSymbolicRepresentation({ permissions }: { permissions: Permissions }): string {
const permissionValue = { read: 'r', write: 'w', execute: 'x' };
const getGroupPermissionValue = (permission: GroupPermissions) =>
_.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, '') : '-'), '');
return [
getGroupPermissionValue(permissions.owner),
getGroupPermissionValue(permissions.group),
getGroupPermissionValue(permissions.public),
].join('');
}

View File

@@ -1,38 +1,8 @@
<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 />
</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 { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service';
import type { Group, Scope } from './chmod-calculator.types';
@@ -52,8 +22,50 @@ const permissions = ref({
});
const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
const symbolic = computed(() => computeChmodSymbolicRepresentation({ permissions: permissions.value }));
</script>
<template>
<div>
<n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
<thead>
<tr>
<th class="text-center" scope="col" />
<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>
<div class="octal-result">
{{ symbolic }}
</div>
<InputCopyable :value="`chmod ${octal} path`" readonly />
</div>
</template>
<style lang="less" scoped>
.octal-result {
text-align: center;

View File

@@ -1,21 +1,6 @@
<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);
@@ -43,6 +28,28 @@ function pause() {
}
</script>
<template>
<div>
<c-card>
<div class="duration">
{{ formatMs(counter) }}
</div>
</c-card>
<div mt-5 flex justify-center gap-3>
<c-button v-if="!isRunning" type="primary" @click="resume">
Start
</c-button>
<c-button v-else type="warning" @click="pause">
Stop
</c-button>
<c-button @click="counter = 0">
Reset
</c-button>
</div>
</div>
</template>
<style lang="less" scoped>
.duration {
text-align: center;

View File

@@ -0,0 +1,23 @@
import { expect, test } from '@playwright/test';
test.describe('Tool - Color converter', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/color-converter');
});
test('Has title', async ({ page }) => {
await expect(page).toHaveTitle('Color converter - IT Tools');
});
test('Color is converted from its name to other formats', async ({ page }) => {
await page.getByTestId('input-name').fill('olive');
expect(await page.getByTestId('input-name').inputValue()).toEqual('olive');
expect(await page.getByTestId('input-hex').inputValue()).toEqual('#808000');
expect(await page.getByTestId('input-rgb').inputValue()).toEqual('rgb(128, 128, 0)');
expect(await page.getByTestId('input-hsl').inputValue()).toEqual('hsl(60, 100%, 25%)');
expect(await page.getByTestId('input-hwb').inputValue()).toEqual('hwb(60 0% 50%)');
expect(await page.getByTestId('input-cmyk').inputValue()).toEqual('device-cmyk(0% 0% 100% 50%)');
expect(await page.getByTestId('input-lch').inputValue()).toEqual('lch(52.15% 56.81 99.57)');
});
});

View File

@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';
import { removeAlphaChannelWhenOpaque } from './color-converter.models';
describe('color-converter models', () => {
describe('removeAlphaChannelWhenOpaque', () => {
it('remove alpha channel of an hex color when it is opaque (alpha = 1)', () => {
expect(removeAlphaChannelWhenOpaque('#000000ff')).toBe('#000000');
expect(removeAlphaChannelWhenOpaque('#ffffffFF')).toBe('#ffffff');
expect(removeAlphaChannelWhenOpaque('#000000FE')).toBe('#000000FE');
expect(removeAlphaChannelWhenOpaque('#00000000')).toBe('#00000000');
});
});
});

View File

@@ -0,0 +1,52 @@
import { type Colord, colord } from 'colord';
import { withDefaultOnError } from '@/utils/defaults';
import { useValidation } from '@/composable/validation';
export { removeAlphaChannelWhenOpaque, buildColorFormat };
function removeAlphaChannelWhenOpaque(hexColor: string) {
return hexColor.replace(/^(#(?:[0-9a-f]{3}){1,2})ff$/i, '$1');
}
function buildColorFormat({
label,
parse = value => colord(value),
format,
placeholder,
invalidMessage = `Invalid ${label.toLowerCase()} format.`,
type = 'text',
}: {
label: string
parse?: (value: string) => Colord
format: (value: Colord) => string
placeholder?: string
invalidMessage?: string
type?: 'text' | 'color-picker'
}) {
const value = ref('');
return {
type,
label,
parse: (v: string) => withDefaultOnError(() => parse(v), undefined),
format,
placeholder,
value,
validation: useValidation({
source: value,
rules: [
{
message: invalidMessage,
validator: v => withDefaultOnError(() => {
if (v === '') {
return true;
}
return parse(v).isValid();
}, false),
},
],
}),
};
}

View File

@@ -1,69 +1,103 @@
<template>
<n-card>
<n-form label-width="100" label-placement="left">
<n-form-item label="color picker:">
<n-color-picker
v-model:value="hex"
placement="bottom-end"
@update:value="(v: string) => onInputUpdated(v, 'hex')"
/>
</n-form-item>
<n-form-item label="color name:">
<input-copyable v-model:value="name" :on-input="(v: string) => onInputUpdated(v, 'name')" />
</n-form-item>
<n-form-item label="hex:">
<input-copyable v-model:value="hex" :on-input="(v: string) => onInputUpdated(v, 'hex')" />
</n-form-item>
<n-form-item label="rgb:">
<input-copyable v-model:value="rgb" :on-input="(v: string) => onInputUpdated(v, 'rgb')" />
</n-form-item>
<n-form-item label="hsl:">
<input-copyable v-model:value="hsl" :on-input="(v: string) => onInputUpdated(v, 'hsl')" />
</n-form-item>
<n-form-item label="hwb:">
<input-copyable v-model:value="hwb" :on-input="(v: string) => onInputUpdated(v, 'hwb')" />
</n-form-item>
<n-form-item label="lch:">
<input-copyable v-model:value="lch" :on-input="(v: string) => onInputUpdated(v, 'lch')" />
</n-form-item>
<n-form-item label="cmyk:">
<input-copyable v-model:value="cmyk" :on-input="(v: string) => onInputUpdated(v, 'cmyk')" />
</n-form-item>
</n-form>
</n-card>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { Colord } from 'colord';
import { colord, extend } from 'colord';
import _ from 'lodash';
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';
import { buildColorFormat } from './color-converter.models';
extend([cmykPlugin, hwbPlugin, namesPlugin, lchPlugin]);
const name = ref('');
const hex = ref('#1ea54cff');
const rgb = ref('');
const hsl = ref('');
const hwb = ref('');
const cmyk = ref('');
const lch = ref('');
const formats = {
picker: buildColorFormat({
label: 'color picker',
format: (v: Colord) => v.toHex(),
type: 'color-picker',
}),
hex: buildColorFormat({
label: 'hex',
format: (v: Colord) => v.toHex(),
placeholder: 'e.g. #ff0000',
}),
rgb: buildColorFormat({
label: 'rgb',
format: (v: Colord) => v.toRgbString(),
placeholder: 'e.g. rgb(255, 0, 0)',
}),
hsl: buildColorFormat({
label: 'hsl',
format: (v: Colord) => v.toHslString(),
placeholder: 'e.g. hsl(0, 100%, 50%)',
}),
hwb: buildColorFormat({
label: 'hwb',
format: (v: Colord) => v.toHwbString(),
placeholder: 'e.g. hwb(0, 0%, 0%)',
}),
lch: buildColorFormat({
label: 'lch',
format: (v: Colord) => v.toLchString(),
placeholder: 'e.g. lch(53.24, 104.55, 40.85)',
}),
cmyk: buildColorFormat({
label: 'cmyk',
format: (v: Colord) => v.toCmykString(),
placeholder: 'e.g. cmyk(0, 100%, 100%, 0)',
}),
name: buildColorFormat({
label: 'name',
format: (v: Colord) => v.toName({ closest: true }) ?? 'Unknown',
placeholder: 'e.g. red',
}),
};
function onInputUpdated(value: string, omit: string) {
const color = colord(value);
updateColorValue(colord('#1ea54c'));
if (omit !== 'name') name.value = color.toName({ closest: true }) ?? '';
if (omit !== 'hex') hex.value = color.toHex();
if (omit !== 'rgb') rgb.value = color.toRgbString();
if (omit !== 'hsl') hsl.value = color.toHslString();
if (omit !== 'hwb') hwb.value = color.toHwbString();
if (omit !== 'cmyk') cmyk.value = color.toCmykString();
if (omit !== 'lch') lch.value = color.toLchString();
function updateColorValue(value: Colord | undefined, omitLabel?: string) {
if (value === undefined) {
return;
}
if (!value.isValid()) {
return;
}
_.forEach(formats, ({ value: valueRef, format }, key) => {
if (key !== omitLabel) {
valueRef.value = format(value);
}
});
}
onInputUpdated(hex.value, 'hex');
</script>
<template>
<c-card>
<template v-for="({ label, parse, placeholder, validation, type }, key) in formats" :key="key">
<input-copyable
v-if="type === 'text'"
v-model:value="formats[key].value.value"
:test-id="`input-${key}`"
:label="`${label}:`"
label-position="left"
label-width="100px"
label-align="right"
:placeholder="placeholder"
:validation="validation"
raw-text
clearable
mt-2
@update:value="(v:string) => updateColorValue(parse(v), key)"
/>
<n-form-item v-else-if="type === 'color-picker'" :label="`${label}:`" label-width="100" label-placement="left" :show-feedback="false">
<n-color-picker
v-model:value="formats[key].value.value"
placement="bottom-end"
@update:value="(v:string) => updateColorValue(parse(v), key)"
/>
</n-form-item>
</template>
</c-card>
</template>

View File

@@ -1,92 +1,6 @@
<template>
<n-card>
<n-form-item
class="cron"
:show-label="false"
:feedback="cronValidation.message"
:validation-status="cronValidation.status"
>
<n-input v-model:value="cron" size="large" placeholder="* * * * *" />
</n-form-item>
<div class="cron-string">
{{ cronString }}
</div>
<n-divider />
<n-space justify="center">
<n-form :show-feedback="false" label-width="170" label-placement="left">
<n-form-item label="Verbose">
<n-switch v-model:value="cronstrueConfig.verbose" />
</n-form-item>
<n-form-item label="Use 24 hour time format">
<n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" />
</n-form-item>
<n-form-item label="Days start at 0">
<n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" />
</n-form-item>
</n-form>
</n-space>
</n-card>
<n-card>
<pre>
[optional] seconds (0 - 59)
| minute (0 - 59)
| | hour (0 - 23)
| | | day of month (1 - 31)
| | | | month (1 - 12) OR jan,feb,mar,apr ...
| | | | | day of week (0 - 6, sunday=0) OR sun,mon ...
| | | | | |
* * * * * * command</pre
>
<n-space v-if="styleStore.isSmallScreen" vertical>
<n-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" embedded :bordered="false">
<div>
Symbol: <strong>{{ symbol }}</strong>
</div>
<div>
Meaning: <strong>{{ meaning }}</strong>
</div>
<div>
Example:
<strong
><code>{{ example }}</code></strong
>
</div>
<div>
Equivalent: <strong>{{ equivalent }}</strong>
</div>
</n-card>
</n-space>
<n-table v-else size="small">
<thead>
<tr>
<th class="text-left" scope="col">Symbol</th>
<th class="text-left" scope="col">Meaning</th>
<th class="text-left" scope="col">Example</th>
<th class="text-left" scope="col">Equivalent</th>
</tr>
</thead>
<tbody>
<tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol">
<td>{{ symbol }}</td>
<td>{{ meaning }}</td>
<td>
<code>{{ example }}</code>
</td>
<td>{{ equivalent }}</td>
</tr>
</tbody>
</n-table>
</n-card>
</template>
<script setup lang="ts">
import cronstrue from 'cronstrue';
import { isValidCron } from 'cron-validator';
import { computed, reactive, ref } from 'vue';
import { useValidation } from '@/composable/validation';
import { useStyleStore } from '@/stores/style.store';
function isCronValid(v: string) {
@@ -185,30 +99,85 @@ const cronString = computed(() => {
return ' ';
});
const cronValidation = useValidation({
source: cron,
rules: [
{
validator: (value) => isCronValid(value),
message: 'This cron is invalid',
},
],
});
const cronValidationRules = [
{
validator: (value: string) => isCronValid(value),
message: 'This cron is invalid',
},
];
</script>
<template>
<c-card>
<div mx-auto max-w-sm>
<c-input-text
v-model:value="cron"
size="large"
placeholder="* * * * *"
:validation-rules="cronValidationRules"
mb-3
/>
</div>
<div class="cron-string">
{{ cronString }}
</div>
<n-divider />
<div flex justify-center>
<n-form :show-feedback="false" label-width="170" label-placement="left">
<n-form-item label="Verbose">
<n-switch v-model:value="cronstrueConfig.verbose" />
</n-form-item>
<n-form-item label="Use 24 hour time format">
<n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" />
</n-form-item>
<n-form-item label="Days start at 0">
<n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" />
</n-form-item>
</n-form>
</div>
</c-card>
<c-card>
<pre>
[optional] seconds (0 - 59)
| minute (0 - 59)
| | hour (0 - 23)
| | | day of month (1 - 31)
| | | | month (1 - 12) OR jan,feb,mar,apr ...
| | | | | day of week (0 - 6, sunday=0) OR sun,mon ...
| | | | | |
* * * * * * command</pre>
<div v-if="styleStore.isSmallScreen">
<c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none>
<div>
Symbol: <strong>{{ symbol }}</strong>
</div>
<div>
Meaning: <strong>{{ meaning }}</strong>
</div>
<div>
Example:
<strong><code>{{ example }}</code></strong>
</div>
<div>
Equivalent: <strong>{{ equivalent }}</strong>
</div>
</c-card>
</div>
<c-table v-else :data="helpers" />
</c-card>
</template>
<style lang="less" scoped>
.cron {
::v-deep(input) {
font-size: 30px;
font-family: monospace;
padding: 5px;
text-align: center;
margin: auto;
max-width: 400px;
display: block;
.n-input {
font-size: 30px;
font-family: monospace;
padding: 5px;
}
}
.cron-string {

View File

@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from '@playwright/test';
test.describe('Date time converter - json to yaml', () => {
test.beforeEach(async ({ page }) => {
@@ -29,5 +29,6 @@ test.describe('Date time converter - json to yaml', () => {
expect((await page.getByTestId('Timestamp').inputValue()).trim()).toEqual('1681333824000');
expect((await page.getByTestId('UTC format').inputValue()).trim()).toEqual('Wed, 12 Apr 2023 21:10:24 GMT');
expect((await page.getByTestId('Mongo ObjectID').inputValue()).trim()).toEqual('64371e400000000000000000');
expect((await page.getByTestId('Excel date/time').inputValue()).trim()).toEqual('45028.88222222222');
});
});

View File

@@ -1,13 +1,16 @@
import { describe, test, expect } from 'vitest';
import { describe, expect, test } from 'vitest';
import {
dateToExcelFormat,
excelFormatToDate,
isExcelFormat,
isISO8601DateTimeString,
isISO9075DateString,
isMongoObjectId,
isRFC3339DateString,
isRFC7231DateString,
isUnixTimestamp,
isTimestamp,
isUTCDateString,
isMongoObjectId,
isUnixTimestamp,
} from './date-time-converter.models';
describe('date-time-converter models', () => {
@@ -139,4 +142,39 @@ describe('date-time-converter models', () => {
expect(isMongoObjectId('')).toBe(false);
});
});
describe('isExcelFormat', () => {
test('an Excel format string is a floating number that can be negative', () => {
expect(isExcelFormat('0')).toBe(true);
expect(isExcelFormat('1')).toBe(true);
expect(isExcelFormat('1.1')).toBe(true);
expect(isExcelFormat('-1.1')).toBe(true);
expect(isExcelFormat('-1')).toBe(true);
expect(isExcelFormat('')).toBe(false);
expect(isExcelFormat('foo')).toBe(false);
expect(isExcelFormat('1.1.1')).toBe(false);
});
});
describe('dateToExcelFormat', () => {
test('a date in Excel format is the number of days since 01/01/1900', () => {
expect(dateToExcelFormat(new Date('2016-05-20T00:00:00.000Z'))).toBe('42510');
expect(dateToExcelFormat(new Date('2016-05-20T12:00:00.000Z'))).toBe('42510.5');
expect(dateToExcelFormat(new Date('2023-10-31T09:26:06.421Z'))).toBe('45230.39312987268');
expect(dateToExcelFormat(new Date('1970-01-01T00:00:00.000Z'))).toBe('25569');
expect(dateToExcelFormat(new Date('1800-01-01T00:00:00.000Z'))).toBe('-36522');
});
});
describe('excelFormatToDate', () => {
test('a date in Excel format is the number of days since 01/01/1900', () => {
expect(excelFormatToDate('0')).toEqual(new Date('1899-12-30T00:00:00.000Z'));
expect(excelFormatToDate('1')).toEqual(new Date('1899-12-31T00:00:00.000Z'));
expect(excelFormatToDate('2')).toEqual(new Date('1900-01-01T00:00:00.000Z'));
expect(excelFormatToDate('4242.4242')).toEqual(new Date('1911-08-12T10:10:50.880Z'));
expect(excelFormatToDate('42738.22626859954')).toEqual(new Date('2017-01-03T05:25:49.607Z'));
expect(excelFormatToDate('-1000')).toEqual(new Date('1897-04-04T00:00:00.000Z'));
});
});
});

View File

@@ -9,18 +9,23 @@ export {
isTimestamp,
isUTCDateString,
isMongoObjectId,
dateToExcelFormat,
excelFormatToDate,
isExcelFormat,
};
const ISO8601_REGEX =
/^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
const ISO9075_REGEX =
/^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/;
const ISO8601_REGEX
= /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
const ISO9075_REGEX
= /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/;
const RFC3339_REGEX =
/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/;
const RFC3339_REGEX
= /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/;
const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/;
const EXCEL_FORMAT_REGEX = /^-?\d+(\.\d+)?$/;
function createRegexMatcher(regex: RegExp) {
return (date?: string) => !_.isNil(date) && regex.test(date);
}
@@ -33,6 +38,8 @@ const isUnixTimestamp = createRegexMatcher(/^[0-9]{1,10}$/);
const isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/);
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
const isExcelFormat = createRegexMatcher(EXCEL_FORMAT_REGEX);
function isUTCDateString(date?: string) {
if (_.isNil(date)) {
return false;
@@ -40,7 +47,16 @@ function isUTCDateString(date?: string) {
try {
return new Date(date).toUTCString() === date;
} catch (_ignored) {
}
catch (_ignored) {
return false;
}
}
function dateToExcelFormat(date: Date) {
return String(((date.getTime()) / (1000 * 60 * 60 * 24)) + 25569);
}
function excelFormatToDate(excelFormat: string | number) {
return new Date((Number(excelFormat) - 25569) * 86400 * 1000);
}

View File

@@ -1,8 +1,8 @@
export type ToDateMapper = (value: string) => Date;
export type DateFormat = {
name: string;
fromDate: (date: Date) => string;
toDate: (value: string) => Date;
formatMatcher: (dateString: string) => boolean;
};
export interface DateFormat {
name: string
fromDate: (date: Date) => string
toDate: (value: string) => Date
formatMatcher: (dateString: string) => boolean
}

View File

@@ -1,37 +1,3 @@
<template>
<div>
<n-form-item :show-label="false" v-bind="validation.attrs">
<n-input-group>
<n-input
v-model:value="inputDate"
:on-input="onDateInputChanged"
placeholder="Put you date string here..."
clearable
:input-props="{ 'data-test-id': 'date-time-converter-input' }"
/>
<n-select
v-model:value="formatIndex"
style="flex: 0 0 170px"
:options="formats.map(({ name }, i) => ({ label: name, value: i }))"
data-test-id="date-time-converter-format-select"
/>
</n-input-group>
</n-form-item>
<n-divider style="margin-top: 0" />
<div v-for="{ name, fromDate } in formats" :key="name" mt-1>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> {{ name }}: </n-input-group-label>
<input-copyable
:value="formatDateUsingFormatter(fromDate, normalizedDate)"
placeholder="Invalid date..."
:input-props="{ 'data-test-id': name }"
/>
</n-input-group>
</div>
</div>
</template>
<script setup lang="ts">
import {
formatISO,
@@ -41,41 +7,36 @@ import {
fromUnixTime,
getTime,
getUnixTime,
parseISO,
parseJSON,
isDate,
isValid,
parseISO,
parseJSON,
} from 'date-fns';
import { withDefaultOnError } from '@/utils/defaults';
import { useValidation } from '@/composable/validation';
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
import {
dateToExcelFormat,
excelFormatToDate,
isExcelFormat,
isISO8601DateTimeString,
isISO9075DateString,
isMongoObjectId,
isRFC3339DateString,
isRFC7231DateString,
isTimestamp,
isUTCDateString,
isUnixTimestamp,
isMongoObjectId,
} from './date-time-converter.models';
import { withDefaultOnError } from '@/utils/defaults';
import { useValidation } from '@/composable/validation';
const inputDate = ref('');
const toDate: ToDateMapper = (date) => new Date(date);
function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) {
if (!date || !validation.isValid) {
return '';
}
return withDefaultOnError(() => formatter(date), '');
}
const toDate: ToDateMapper = date => new Date(date);
const formats: DateFormat[] = [
{
name: 'JS locale date string',
fromDate: (date) => date.toString(),
fromDate: date => date.toString(),
toDate,
formatMatcher: () => false,
},
@@ -83,49 +44,55 @@ const formats: DateFormat[] = [
name: 'ISO 8601',
fromDate: formatISO,
toDate: parseISO,
formatMatcher: (date) => isISO8601DateTimeString(date),
formatMatcher: date => isISO8601DateTimeString(date),
},
{
name: 'ISO 9075',
fromDate: formatISO9075,
toDate: parseISO,
formatMatcher: (date) => isISO9075DateString(date),
formatMatcher: date => isISO9075DateString(date),
},
{
name: 'RFC 3339',
fromDate: formatRFC3339,
toDate,
formatMatcher: (date) => isRFC3339DateString(date),
formatMatcher: date => isRFC3339DateString(date),
},
{
name: 'RFC 7231',
fromDate: formatRFC7231,
toDate,
formatMatcher: (date) => isRFC7231DateString(date),
formatMatcher: date => isRFC7231DateString(date),
},
{
name: 'Unix timestamp',
fromDate: (date) => String(getUnixTime(date)),
toDate: (sec) => fromUnixTime(+sec),
formatMatcher: (date) => isUnixTimestamp(date),
fromDate: date => String(getUnixTime(date)),
toDate: sec => fromUnixTime(+sec),
formatMatcher: date => isUnixTimestamp(date),
},
{
name: 'Timestamp',
fromDate: (date) => String(getTime(date)),
toDate: (ms) => parseJSON(+ms),
formatMatcher: (date) => isTimestamp(date),
fromDate: date => String(getTime(date)),
toDate: ms => parseJSON(+ms),
formatMatcher: date => isTimestamp(date),
},
{
name: 'UTC format',
fromDate: (date) => date.toUTCString(),
fromDate: date => date.toUTCString(),
toDate,
formatMatcher: (date) => isUTCDateString(date),
formatMatcher: date => isUTCDateString(date),
},
{
name: 'Mongo ObjectID',
fromDate: (date) => Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000',
toDate: (objectId) => new Date(parseInt(objectId.substring(0, 8), 16) * 1000),
formatMatcher: (date) => isMongoObjectId(date),
fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`,
toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000),
formatMatcher: date => isMongoObjectId(date),
},
{
name: 'Excel date/time',
fromDate: date => dateToExcelFormat(date),
toDate: excelFormatToDate,
formatMatcher: isExcelFormat,
},
];
@@ -141,7 +108,8 @@ const normalizedDate = computed(() => {
try {
return toDate(inputDate.value);
} catch (_ignored) {
}
catch (_ignored) {
return undefined;
}
});
@@ -159,9 +127,11 @@ const validation = useValidation({
rules: [
{
message: 'This date is invalid for this format',
validator: (value) =>
validator: value =>
withDefaultOnError(() => {
if (value === '') return true;
if (value === '') {
return true;
}
const maybeDate = formats[formatIndex.value].toDate(value);
return isDate(maybeDate) && isValid(maybeDate);
@@ -169,4 +139,51 @@ const validation = useValidation({
},
],
});
function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) {
if (!date || !validation.isValid) {
return '';
}
return withDefaultOnError(() => formatter(date), '');
}
</script>
<template>
<div>
<div flex gap-2>
<c-input-text
v-model:value="inputDate"
autofocus
placeholder="Put your date string here..."
clearable
test-id="date-time-converter-input"
:validation="validation"
@update:value="onDateInputChanged"
/>
<c-select
v-model:value="formatIndex"
style="flex: 0 0 170px"
:options="formats.map(({ name }, i) => ({ label: name, value: i }))"
data-test-id="date-time-converter-format-select"
/>
</div>
<n-divider />
<input-copyable
v-for="{ name, fromDate } in formats"
:key="name"
:label="name"
label-width="150px"
label-position="left"
label-align="right"
:value="formatDateUsingFormatter(fromDate, normalizedDate)"
placeholder="Invalid date..."
:test-id="name"
readonly
mt-2
/>
</div>
</template>

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