Compare commits

...

123 Commits

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

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

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

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

* feat(ipv4-range-expander): only show n-alert if both ipv4 addresses are valid
2023-04-19 20:30:45 +02:00
Corentin Thomasset
6d2202597c feat(date converter): auto focus main input 2023-04-19 13:07:24 +02:00
Corentin Thomasset
c68a1fd713 chore(version): release 2023.04.14-dbad773 2023-04-14 21:09:36 +02:00
Corentin Thomasset
46b1a07213 docs(changelog): update changelog for 2023.04.14-dbad773 2023-04-14 21:09:36 +02:00
Corentin Thomasset
dbad7730f9 chore(release): create a github release on new version 2023-04-14 21:08:43 +02:00
Corentin Thomasset
85cb0ffabd chore(version): reset CHANGELOG content to support new format 2023-04-14 18:34:23 +02:00
Corentin Thomasset
8355bd2ae4 feat(new-tool): http status codes 2023-04-14 09:04:49 +02:00
Corentin Thomasset
6fb4994603 refactor(uuid-generator): prevent NaN in quantity 2023-04-13 23:36:25 +02:00
Corentin Thomasset
7d7cc99866 chore(version): release v2023.04.13-dce9ff9 2023-04-13 01:09:14 +02:00
Corentin Thomasset
dce9ff91e2 feat(cd): git version tag pushed to docker 2023-04-13 01:04:48 +02:00
Corentin Thomasset
80401b6405 refactor(404): simplified 404 page 2023-04-13 00:03:08 +02:00
Corentin Thomasset
fd9ab59172 refactor(date-converter): improved ux and layout 2023-04-12 23:41:09 +02:00
Corentin Thomasset
5fa811a583 chore(deps): updated dependencies 2023-04-12 20:35:44 +02:00
Corentin Thomasset
c0a89131dd feat(new-tool): yaml and json converters 2023-04-10 22:38:35 +02:00
Corentin Thomasset
05f06f6a07 refactor(dx): generic data transformer 2023-04-10 16:42:06 +02:00
Corentin Thomasset
9fa4c26929 refactor(config): removed new tools env config 2023-04-10 13:11:10 +02:00
Corentin Thomasset
9d639edf2d refactor(lint): auto fix lint 2023-04-10 13:01:40 +02:00
cgoIT
a1e983538c feat(new-tool): generate ula based on timestamp and mac address (#344)
* feat(ipv6-ula-generator): new tool: generate ula based on timestamp and mac address

This new tool generates a random unique ula based on the current timestamp and the provided mac address. An ULA is your "secondary" IPV6-Address only for internal use.
It can also be used as a backup address if your provider gets offline and your Prefix-IPs are not longer valid.Also you can create the most of your internal firewall rules based on your ULAs.

* feat(ipv6-ula-generator): changes requested by review

* Update src/tools/ipv6-ula-generator/index.ts

* Update src/tools/ipv6-ula-generator/ipv6-ula-generator.vue

---------

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
2023-04-09 21:05:26 +02:00
cgoIT
1d7f8b9a8c feat(new-tool): generate ula based on timestamp and mac address (#344)
* feat(ipv6-ula-generator): new tool: generate ula based on timestamp and mac address

This new tool generates a random unique ula based on the current timestamp and the provided mac address. An ULA is your "secondary" IPV6-Address only for internal use.
It can also be used as a backup address if your provider gets offline and your Prefix-IPs are not longer valid.Also you can create the most of your internal firewall rules based on your ULAs.

* feat(ipv6-ula-generator): changes requested by review

* Update src/tools/ipv6-ula-generator/index.ts

* Update src/tools/ipv6-ula-generator/ipv6-ula-generator.vue

---------

Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
2023-04-09 20:58:27 +02:00
Corentin Thomasset
ec7cb9351c feat(test): added e2e tests 2023-04-09 18:16:22 +02:00
Corentin Thomasset
ebfdb64fde chore(cd): don't run nightly release if their is no new commits 2023-04-09 15:27:58 +02:00
cgoIT
d2a2686705 fix(mac-address-lookup): fix typo in mac-address-lookup tool (#345) 2023-04-09 14:03:17 +02:00
Corentin Thomasset
d7a503b4ae feat(new-tool): ipv4 address converter 2023-04-08 22:09:22 +02:00
Corentin Thomasset
28145e0ffe feat(new-tool): ipv4 address converter 2023-04-08 21:13:14 +02:00
cgoIT
8930e139b2 fix(roman-numeral-converter): input validation and feedback (#332)
* fix(roman-numeral-converter):  checks for valid input and conversion enhancements

Validates if numeral values are between 1 and 3999999.
Validates if a roman number is valid.

* fix(roman-numeral-converter): optimize logic for copy button

* fix(roman-numeral-converter): changes due to review
2023-04-08 19:33:33 +02:00
Corentin Thomasset
076df11024 feat(new-tool): mac address lookup 2023-04-07 00:24:49 +02:00
Corentin Thomasset
1060652590 chore(cd): docker nightly releases 2023-04-06 21:44:11 +02:00
Corentin Thomasset
f5c865b278 chore(cd): docker nightly releases 2023-04-06 21:05:56 +02:00
Corentin Thomasset
001031b7b5 feat(css): added unocss with attributify preset 2023-04-06 20:35:37 +02:00
Corentin Thomasset
4ccd73c2d1 feat(dx): auto resolve vue components 2023-04-06 19:08:28 +02:00
Corentin Thomasset
2293f63a79 fix(ci): prevent ci from running twice in pr 2023-04-06 18:56:31 +02:00
Corentin Thomasset
0161395585 feat(dx): auto import vite plugin 2023-04-06 18:53:31 +02:00
Corentin Thomasset
4d011f15a1 feat(benchmark-builder): compute gap and export to bulet list 2023-04-06 14:00:54 +02:00
Corentin Thomasset
4a1afb2b69 feat(benchmark-builder): compute gap and export to bulet list 2023-04-06 13:33:51 +02:00
cgoIT
f350dc19aa feat(new-tool): add new tool user agent parser (#329)
* fix(docker-run-to-docker-compose-converter): use different version of converter which suppports more options and is mor failsafe

* chore(docker-run-to-docker-compose-converter): add pnpm-lock.yaml again which was accidently removed in last commit

* chore(docker-run-to-docker-compose-converter): add fixed version of composerize-ts

* chore(user-agent-parser): changes requested by code review

* chore(user-agent-parser): some more changes requested by code review
2023-04-06 10:28:12 +02:00
Corentin Thomasset
f3480fe560 refactor(qr-code): multiline input 2023-04-06 00:55:39 +02:00
Corentin Thomasset
f68e859c20 refactor(docker-run-converter): improve error handling 2023-04-06 00:16:32 +02:00
cgoIT
4872d71165 refactor(docker-run-converter): improved converter
* fix(docker-run-to-docker-compose-converter): use different version of converter which suppports more options and is mor failsafe

* chore(docker-run-to-docker-compose-converter): add pnpm-lock.yaml again which was accidently removed in last commit

* chore(docker-run-to-docker-compose-converter): add fixed version of composerize-ts
2023-04-06 00:10:19 +02:00
Corentin Thomasset
fb8a3a0fee refactor(tool): better new tool logic 2023-04-05 23:30:44 +02:00
Corentin Thomasset
20282987e3 feat(new-tool): simple benchmark calculator 2023-04-05 23:07:13 +02:00
Corentin Thomasset
ff5f38bec6 feat(new-tool): simple benchmark calculator 2023-04-05 23:06:05 +02:00
Corentin Thomasset
6e84ea4061 feat(new-tool): simple benchmark calculator 2023-04-05 22:57:58 +02:00
Corentin Thomasset
004cb83719 chore(clean): removed extra console.log 2023-04-05 08:47:54 +02:00
Corentin Thomasset
aa4dc0418e feat(plausible): disable plausible instance for self hosted instance 2023-04-04 19:01:59 +02:00
Corentin Thomasset
a599528044 fix(otp-generator): editable secret 2023-04-04 12:55:02 +02:00
Radu Ursache
b3390f6ff7 chore(docker): fixed the docker-run command in README (#323) 2023-03-31 16:53:32 +02:00
Corentin Thomasset
dee5586bf5 fix(docker): consistent casing in Dockerfile 2023-03-31 01:10:00 +02:00
Corentin Thomasset
5281824b5d fix(otp-generator): better computation of token 2023-03-31 01:01:44 +02:00
Corentin Thomasset
15cb03347c feat(menu): collapsible category 2023-03-30 23:51:23 +02:00
Corentin Thomasset
24ba0ff5fa feat(menu): collapsible category 2023-03-30 23:48:47 +02:00
Corentin Thomasset
849981d1ec refactor(issue): better issue template 2023-03-30 21:25:39 +02:00
Corentin Thomasset
3b625fd473 feat(json-prettify): sort keys 2023-03-29 23:33:30 +02:00
Corentin Thomasset
863c8d0f6a refactor(new-tool-script): added new tool creation script in package.json 2023-03-29 22:43:51 +02:00
Corentin Thomasset
9b4f7727f2 refactor(uuid-generator): now using computedRefreshable 2023-03-29 21:13:49 +02:00
Corentin Thomasset
cf16cb195d refactor(token-generator): now using computedRefreshable 2023-03-29 21:12:20 +02:00
Corentin Thomasset
f6237376e1 refactor(port-generator): now using computedRefreshable 2023-03-29 21:05:21 +02:00
Corentin Thomasset
165dc93f83 feat(rsa-key-pair-generator): refresh certs button 2023-03-29 20:54:11 +02:00
Corentin Thomasset
161a21f285 feat(config): hide the banner by default 2023-03-29 19:55:40 +02:00
Corentin Thomasset
972ffe6f69 fix(docker): nginx config to redirect to index.html 2023-03-29 00:15:56 +02:00
Corentin Thomasset
c339ab3551 feat(new-tool): IPv4 subnet calculator 2023-03-28 23:49:54 +02:00
Corentin Thomasset
47948dd343 refactor(import): removed unused imports 2023-03-28 23:49:54 +02:00
marvin-j97
91369cb238 refactor(html-editor): use correct letter casing (#292) 2023-03-27 17:38:37 +02:00
cgoIT
ffb99579ba feat(new-tool): add a docker run to docker compose converter (#291)
* Integrate composerize (https://github.com/magicmark/composerize) into it-tools

* Fix Prettier errors

* Fix Prettier errors

* Fix code according to suggestions

* Added syntax highlighting to TextareaCopyable component, some ui adjustments in docker-run-to-docker-compose-converter.vue
2023-03-27 17:31:13 +02:00
Corentin Thomasset
f512d09227 fix(rsa-key-pair-generator): correct prime web worker url 2023-03-27 00:17:36 +02:00
Corentin Thomasset
7c40539ef9 refactor(otp-generator): coerce lowercase secret characters to uppercase 2023-03-26 22:43:56 +02:00
Corentin Thomasset
f3b1863f09 feat(new-tool): html wysiwyg editor 2023-03-26 20:21:00 +02:00
Corentin Thomasset
b1d6bfd2dc refactor(otp-generator): no more barcode color invertion 2023-03-26 16:15:37 +02:00
Corentin Thomasset
8787ce72ab fix(roman-converter): replaced incorrect default value 2023-03-24 18:35:37 +01:00
Corentin Thomasset
dab8d63390 refactor(readme): better headings levels 2023-03-24 08:42:28 +01:00
Corentin Thomasset
96aaa35ea2 docs(docker): added self hosting command in README 2023-03-23 22:47:14 +01:00
Benny
519b169336 chore(docker): arm64 and amd64 as supported platform (#271) 2023-03-23 21:48:07 +01:00
Corentin Thomasset
3bc1f0d1fa chore(docker): it-tools docker container 2023-03-23 18:54:51 +01:00
Corentin Thomasset
b519cc9574 chore(deps): updated dependencies versions 2023-03-19 11:59:43 +01:00
Corentin Thomasset
30e9149d61 feat(new-tool): RSA key pair generator 2023-03-10 18:16:55 +01:00
marvin-j97
7958e2e075 refactor(branding): correct branding for twitter & github (#268) 2023-03-08 18:09:53 +01:00
Corentin Thomasset
61024279b5 fix(about): correct git branch in package.json url 2023-03-08 13:48:52 +01:00
Corentin Thomasset
ad202bd372 revert(pwa): fallback to autoUpdate 2023-03-01 23:35:17 +01:00
Corentin Thomasset
400654b6b1 chore(pwa): close update notification on update 2023-03-01 23:24:17 +01:00
Corentin Thomasset
3351b70c1a refactor(sponsor): updated support link to buy me a coffe 2023-03-01 22:15:51 +01:00
Corentin Thomasset
53ce079dff refactor(pwa): prompt for pwa update 2023-03-01 22:00:29 +01:00
Corentin Thomasset
a771346250 refactor(style): improved dark mode colors 2023-02-21 23:14:27 +01:00
Corentin Thomasset
a312dedf65 fix(jwt-parser): prevent [object Object] value 2023-02-21 22:52:08 +01:00
Corentin Thomasset
0ddf18f4b5 feat(new-tool): String to NATO alphabet 2023-02-15 00:45:20 +01:00
Corentin Thomasset
9634f5d9a8 fix(case-converter): allowed diacritics 2023-02-10 22:59:44 +01:00
Corentin Thomasset
e6c0445684 fix(case-converter): allowed diacritics 2023-02-10 22:53:10 +01:00
Corentin Thomasset
32f87f3fca refactor(tool-card): enforce description height 2023-02-10 22:20:50 +01:00
Corentin Thomasset
a3b1cb5737 refactor(tool-card): enforce description height 2023-02-10 22:13:08 +01:00
Corentin Thomasset
6fe4b5ac60 feat(new-tool): slugify string 2023-02-10 22:06:32 +01:00
Corentin Thomasset
1a3f0a135d refactor(keycode-info): wording 2023-02-06 23:26:40 +01:00
Corentin Thomasset
072083832d chore(release): 2.19.0 2023-02-06 23:14:52 +01:00
Corentin Thomasset
c934c4e50c feat(new-tool): keycode info 2023-02-06 23:13:20 +01:00
Corentin Thomasset
4a5734d4a3 chore(release): 2.18.0 2023-02-04 09:59:56 +01:00
Tsonglew
f708f5091e feat(new-tool): json minify (#265)
Co-authored-by: Corentin THOMASSET <corentin.thomasset74@gmail.com>
2023-02-04 09:56:17 +01:00
Corentin Thomasset
db817a2459 refactor(tools): config in query params 2023-01-28 21:06:16 +01:00
Corentin Thomasset
119041c185 chore(release): 2.17.0 2023-01-13 18:27:39 +01:00
Corentin Thomasset
4607837f9a feat(new-tool): temperature converter 2023-01-13 18:26:28 +01:00
Corentin Thomasset
f52f7a845c refactor(jwt-parser): simplified code 2023-01-13 14:02:44 +01:00
bastantoine
acc7f0a586 feat(new-tool): jwt parser (#262)
* npm install jwt-decode

* added base tool structure

* added function to decode JWT and display header and payload

* use a table to display the data

* show human readable values

* added switch to toggle display of parsed values

* lint

* replaced basic package-lock.json with pnpm-lock.json

* change the icon of the tool

* simplify return

* use camelCase

* added description of the tool

* always parse the values

* use camelCase...
2022-12-27 09:38:35 +01:00
Corentin Thomasset
ebb7301a98 chore(release): 2.16.0 2022-12-21 21:15:16 +01:00
Corentin Thomasset
def60e7248 refactor(tracker): better tracker injection 2022-12-21 21:02:57 +01:00
Corentin Thomasset
bf88836dbe feat(search-bar): use cmd + k to focus on mac 2022-12-21 00:21:12 +01:00
Corentin Thomasset
bfc2e24bbf feat(tracker): added actions monitoring 2022-12-21 00:03:31 +01:00
Corentin Thomasset
40872859a5 refactor(clean): removed unused import 2022-12-20 21:15:00 +01:00
Corentin Thomasset
cf723f144e refactor(clean): removed empty style tag 2022-12-20 21:14:40 +01:00
Corentin Thomasset
7f964941d3 chore(docs): updated readme 2022-12-20 20:57:24 +01:00
Corentin Thomasset
af075dcccc feat(tool): improved favorite tool management 2022-12-17 15:33:52 +01:00
Corentin Thomasset
274ff02b54 chore(git): added .env to gitignore 2022-12-17 11:42:47 +01:00
Corentin Thomasset
679dd1c1f6 refactor(menu): improve support button 2022-12-17 01:30:27 +01:00
Corentin Thomasset
4cd809bd0c feat(tools): added favorite tool handling 2022-12-17 01:30:02 +01:00
211 changed files with 11554 additions and 3999 deletions

264
.eslintrc-auto-import.json Normal file
View File

@@ -0,0 +1,264 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"EffectScope": true,
"InjectionKey": true,
"PropType": true,
"Ref": true,
"VNode": true,
"asyncComputed": true,
"autoResetRef": true,
"computed": true,
"computedAsync": true,
"computedEager": true,
"computedInject": true,
"computedWithControl": true,
"controlledComputed": true,
"controlledRef": true,
"createApp": true,
"createEventHook": true,
"createGlobalState": true,
"createInjectionState": true,
"createReactiveFn": true,
"createSharedComposable": true,
"createUnrefFn": true,
"customRef": true,
"debouncedRef": true,
"debouncedWatch": true,
"defineAsyncComponent": true,
"defineComponent": true,
"eagerComputed": true,
"effectScope": true,
"extendRef": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"ignorableWatch": true,
"inject": true,
"isDefined": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"logicAnd": true,
"logicNot": true,
"logicOr": true,
"makeDestructurable": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onClickOutside": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onKeyStroke": true,
"onLongPress": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onStartTyping": true,
"onUnmounted": true,
"onUpdated": true,
"pausableWatch": true,
"provide": true,
"reactify": true,
"reactifyObject": true,
"reactive": true,
"reactiveComputed": true,
"reactiveOmit": true,
"reactivePick": true,
"readonly": true,
"ref": true,
"refAutoReset": true,
"refDebounced": true,
"refDefault": true,
"refThrottled": true,
"refWithControl": true,
"resolveComponent": true,
"resolveRef": true,
"resolveUnref": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"syncRef": true,
"syncRefs": true,
"templateRef": true,
"throttledRef": true,
"throttledWatch": true,
"toRaw": true,
"toReactive": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"tryOnBeforeMount": true,
"tryOnBeforeUnmount": true,
"tryOnMounted": true,
"tryOnScopeDispose": true,
"tryOnUnmounted": true,
"unref": true,
"unrefElement": true,
"until": true,
"useActiveElement": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
"useBase64": true,
"useBattery": true,
"useBluetooth": true,
"useBreakpoints": true,
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useCached": true,
"useClamp": true,
"useClipboard": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCounter": true,
"useCssModule": true,
"useCssVar": true,
"useCssVars": true,
"useCurrentElement": true,
"useCycleList": true,
"useDark": true,
"useDateFormat": true,
"useDebounce": true,
"useDebounceFn": true,
"useDebouncedRefHistory": true,
"useDeviceMotion": true,
"useDeviceOrientation": true,
"useDevicePixelRatio": true,
"useDevicesList": true,
"useDialog": true,
"useDisplayMedia": true,
"useDocumentVisibility": true,
"useDraggable": true,
"useDropZone": true,
"useElementBounding": true,
"useElementByPoint": true,
"useElementHover": true,
"useElementSize": true,
"useElementVisibility": true,
"useEventBus": true,
"useEventListener": true,
"useEventSource": true,
"useEyeDropper": true,
"useFavicon": true,
"useFetch": true,
"useFileDialog": true,
"useFileSystemAccess": true,
"useFocus": true,
"useFocusWithin": true,
"useFps": true,
"useFullscreen": true,
"useGamepad": true,
"useGeolocation": true,
"useIdle": true,
"useImage": true,
"useInfiniteScroll": true,
"useIntersectionObserver": true,
"useInterval": true,
"useIntervalFn": true,
"useKeyModifier": true,
"useLastChanged": true,
"useLink": true,
"useLoadingBar": true,
"useLocalStorage": true,
"useMagicKeys": true,
"useManualRefHistory": true,
"useMediaControls": true,
"useMediaQuery": true,
"useMemoize": true,
"useMemory": true,
"useMessage": true,
"useMounted": true,
"useMouse": true,
"useMouseInElement": true,
"useMousePressed": true,
"useMutationObserver": true,
"useNavigatorLanguage": true,
"useNetwork": true,
"useNotification": true,
"useNow": true,
"useObjectUrl": true,
"useOffsetPagination": true,
"useOnline": true,
"usePageLeave": true,
"useParallax": true,
"usePermission": true,
"usePointer": true,
"usePointerSwipe": true,
"usePreferredColorScheme": true,
"usePreferredDark": true,
"usePreferredLanguages": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
"useRoute": true,
"useRouter": true,
"useScreenOrientation": true,
"useScreenSafeArea": true,
"useScriptTag": true,
"useScroll": true,
"useScrollLock": true,
"useSessionStorage": true,
"useShare": true,
"useSlots": true,
"useSpeechRecognition": true,
"useSpeechSynthesis": true,
"useStepper": true,
"useStorage": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSwipe": true,
"useTemplateRefsList": true,
"useTextSelection": true,
"useTextareaAutosize": true,
"useThrottle": true,
"useThrottleFn": true,
"useThrottledRefHistory": true,
"useTimeAgo": true,
"useTimeout": true,
"useTimeoutFn": true,
"useTimeoutPoll": true,
"useTimestamp": true,
"useTitle": true,
"useToggle": true,
"useTransition": true,
"useUrlSearchParams": true,
"useUserMedia": true,
"useVModel": true,
"useVModels": true,
"useVibrate": true,
"useVirtualList": true,
"useWakeLock": true,
"useWebNotification": true,
"useWebSocket": true,
"useWebWorker": true,
"useWebWorkerFn": true,
"useWindowFocus": true,
"useWindowScroll": true,
"useWindowSize": true,
"watch": true,
"watchArray": true,
"watchAtMost": true,
"watchDebounced": true,
"watchEffect": true,
"watchIgnorable": true,
"watchOnce": true,
"watchPausable": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"watchThrottled": true,
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true
}
}

View File

@@ -10,6 +10,8 @@ module.exports = {
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier',
'plugin:import/recommended',
'./.eslintrc-auto-import.json',
'@unocss',
],
settings: {
@@ -32,5 +34,6 @@ module.exports = {
tsx: 'never',
},
],
'import/no-unresolved': ['error', { ignore: ['^virtual:'] }],
},
};

View File

@@ -1,17 +1,17 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] "
about: Create a report to help us improve our tools
title: '[BUG] '
labels: bug
assignees: CorentinTh
---
**Describe the bug**
A clear and concise description of what the bug is.
**Which tool is impacted?**
Example: the token generator
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -24,10 +24,11 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Configuration (please complete the following information):**
- Device: [e.g. iPhone6, ]
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
- Device: [e.g. iPhone6, ]
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEAT]"
labels: enhancement
assignees: CorentinTh
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,19 @@
---
name: New tool request
about: Suggest a new tool idea
title: '[NEW TOOL]'
labels: new tool
assignees: CorentinTh
---
**Which tool is impacted?**
Example: the token generator
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Is their example of this tool in the wild?**
Provide link to already existing tool or npm packages if any exists
**Additional context**
Add any other context about the feature request here.

13
.github/ISSUE_TEMPLATE/other-request.md vendored Normal file
View File

@@ -0,0 +1,13 @@
---
name: Other request
about: Any request that does not concern a tool creation, a new feature request on a tool or a bug
title: '[OTHER] '
labels:
assignees: CorentinTh
---
**Describe the solution you'd like**
A clear and concise description of what you want.
**Additional context**
Add any other context about the feature request here.

View File

@@ -0,0 +1,13 @@
---
name: Tool improvement
about: Improvement on an existing tool
title: '[TOOL IMPROVEMENT]'
labels: enhancement
assignees: CorentinTh
---
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context about the feature request here.

View File

@@ -1,6 +1,10 @@
name: ci
on: [push, pull_request]
on:
pull_request:
push:
branches:
- main
jobs:
ci:
@@ -23,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

@@ -0,0 +1,87 @@
name: Docker nightly release
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
check_date:
runs-on: ubuntu-latest
name: Check latest commit
outputs:
should_run: ${{ steps.should_run.outputs.should_run }}
steps:
- uses: actions/checkout@v2
- name: print latest_commit
run: echo ${{ github.sha }}
- id: should_run
continue-on-error: true
name: check latest commit is less than a day
if: ${{ github.event_name == 'schedule' }}
run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false"
ci:
runs-on: ubuntu-latest
needs: check_date
if: ${{ needs.check_date.outputs.should_run != 'false' }}
steps:
- uses: actions/checkout@v3
- run: corepack enable
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Run linters
run: pnpm lint
- name: Run unit test
run: pnpm test
- name: Build the app
run: pnpm build
build:
runs-on: ubuntu-latest
needs:
- ci
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
corentinth/it-tools:nightly
ghcr.io/corentinth/it-tools:nightly

23
.github/workflows/playwright.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
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

104
.github/workflows/releases.yml vendored Normal file
View File

@@ -0,0 +1,104 @@
name: Release new versions
on:
push:
tags:
- 'v*.*.*'
jobs:
docker-release:
runs-on: ubuntu-latest
steps:
- name: Get release version
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
corentinth/it-tools:latest
corentinth/it-tools:${{ env.RELEASE_VERSION }}
ghcr.io/corentinth/it-tools:latest
ghcr.io/corentinth/it-tools:${{ env.RELEASE_VERSION}}
github-release:
runs-on: ubuntu-latest
needs: docker-release
steps:
- name: Get release version
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
- run: corepack enable
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Build the app
run: pnpm build
- name: Zip the app
run: zip -r it-tools-${{ env.RELEASE_VERSION }}.zip dist/*
- name: Get changelog
id: changelog
run: |
EOF=$(openssl rand -hex 8)
echo "changelog<<$EOF" >> $GITHUB_OUTPUT
node ./scripts/getLatestChangelog.mjs >> $GITHUB_OUTPUT
echo "$EOF" >> $GITHUB_OUTPUT
- name: Create Release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: it-tools-${{ env.RELEASE_VERSION }}.zip
tag_name: v${{ env.RELEASE_VERSION }}
draft: true
prerelease: false
body: |
## Docker images
- Docker Hub
- `corentinth/it-tools:latest`
- `corentinth/it-tools:${{ env.RELEASE_VERSION }}`
- GitHub Container Registry
- `ghcr.io/corentinth/it-tools:latest`
- `ghcr.io/corentinth/it-tools:${{ env.RELEASE_VERSION}}`
## Changelog
${{ steps.changelog.outputs.changelog }}

5
.gitignore vendored
View File

@@ -26,3 +26,8 @@ coverage
*.njsproj
*.sln
*.sw?
.env
/test-results/
/playwright-report/
/playwright/.cache/

View File

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

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
# build stage
FROM node:lts-alpine AS build-stage
WORKDIR /app
COPY . .
RUN npm install -g pnpm
RUN pnpm i --frozen-lockfile
RUN pnpm build
# production stage
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;"]

View File

@@ -8,48 +8,72 @@ Please check the [issues](https://github.com/CorentinTh/it-tools/issues) to see
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=)!
## Self host
Self host solutions for your homelab
**From docker hub:**
```sh
docker run -d --name it-tools --restart unless-stopped -p 8080:80 corentinth/it-tools:latest
```
**From github packages:**
```sh
docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corentinth/it-tools:latest
```
**Other solutions:**
- [Tipi](https://www.runtipi.io/docs/apps-available)
- [Unraid](https://unraid.net/community/apps?q=it-tools)
## Contribute
### Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin).
[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).
### Node version
### Type Support for `.vue` Imports in TS
Ensure you have the correct node/npm version
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
```sh
nvm use
```
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
### Project Setup
```sh
npm install
pnpm install
```
#### Compile and Hot-Reload for Development
### Compile and Hot-Reload for Development
```sh
npm run dev
pnpm dev
```
#### Type-Check, Compile and Minify for Production
### Type-Check, Compile and Minify for Production
```sh
npm run build
pnpm build
```
#### Run Unit Tests with [Vitest](https://vitest.dev/)
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test
pnpm test
```
#### Lint with [ESLint](https://eslint.org/)
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
pnpm lint
```
### Create a new tool
@@ -57,7 +81,7 @@ npm run lint
To create a new tool, there is a script that generate the boilerplate of the new tool, simply run:
```sh
node scripts/create-tool.mjs my-tool-name
pnpm run script:create-new-tool my-tool-name
```
It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the inported tool in the proper category and develop the tool.
@@ -68,12 +92,9 @@ Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr).
This project is continuously deployed using [vercel.com](https://vercel.com).
<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>
## License
This project is under the [GNU GPLv3](LICENSE).

524
auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,524 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-auto-import
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
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']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
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 useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDialog: typeof import('naive-ui')['useDialog']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router')['useLink']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMessage: typeof import('naive-ui')['useMessage']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNotification: typeof import('naive-ui')['useNotification']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
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 useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
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 createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
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 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']>
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 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 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 useClamp: UnwrapRef<typeof import('@vueuse/core')['useClamp']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
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 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 usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
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 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 useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
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 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 watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
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']>
}
}

152
components.d.ts vendored Normal file
View File

@@ -0,0 +1,152 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
'404.page': typeof import('./src/pages/404.page.vue')['default']
About: typeof import('./src/pages/About.vue')['default']
App: typeof import('./src/App.vue')['default']
'Base.layout': typeof import('./src/layouts/base.layout.vue')['default']
Base64FileConverter: typeof import('./src/tools/base64-file-converter/base64-file-converter.vue')['default']
Base64StringConverter: typeof import('./src/tools/base64-string-converter/base64-string-converter.vue')['default']
BasicAuthGenerator: typeof import('./src/tools/basic-auth-generator/basic-auth-generator.vue')['default']
Bcrypt: typeof import('./src/tools/bcrypt/bcrypt.vue')['default']
BenchmarkBuilder: typeof import('./src/tools/benchmark-builder/benchmark-builder.vue')['default']
Bip39Generator: typeof import('./src/tools/bip39-generator/bip39-generator.vue')['default']
CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default']
CButton: typeof import('./src/ui/c-button/c-button.vue')['default']
'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default']
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
CLink: typeof import('./src/ui/c-link/c-link.vue')['default']
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
CopyableIpLike: typeof import('./src/tools/ipv4-subnet-calculator/copyable-ip-like.vue')['default']
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default']
'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default']
DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default']
DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default']
DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default']
DockerRunToDockerComposeConverter: typeof import('./src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue')['default']
DynamicValues: typeof import('./src/tools/benchmark-builder/dynamic-values.vue')['default']
Editor: typeof import('./src/tools/html-wysiwyg-editor/editor/editor.vue')['default']
Encryption: typeof import('./src/tools/encryption/encryption.vue')['default']
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
GitMemo: typeof import('./src/tools/git-memo/git-memo.md')['default']
HashText: typeof import('./src/tools/hash-text/hash-text.vue')['default']
HmacGenerator: typeof import('./src/tools/hmac-generator/hmac-generator.vue')['default']
'Home.page': typeof import('./src/pages/Home.page.vue')['default']
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
Ipv4RangeExpander: typeof import('./src/tools/ipv4-range-expander/ipv4-range-expander.vue')['default']
Ipv4SubnetCalculator: typeof import('./src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue')['default']
Ipv6UlaGenerator: typeof import('./src/tools/ipv6-ula-generator/ipv6-ula-generator.vue')['default']
JsonDiff: typeof import('./src/tools/json-diff/json-diff.vue')['default']
JsonMinify: typeof import('./src/tools/json-minify/json-minify.vue')['default']
JsonToYaml: typeof import('./src/tools/json-to-yaml-converter/json-to-yaml.vue')['default']
JsonViewer: typeof import('./src/tools/json-viewer/json-viewer.vue')['default']
JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default']
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default']
MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default']
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NAutoComplete: typeof import('naive-ui')['NAutoComplete']
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
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']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDatePicker: typeof import('naive-ui')['NDatePicker']
NDivider: typeof import('naive-ui')['NDivider']
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1']
NH2: typeof import('naive-ui')['NH2']
NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
NInputNumber: typeof import('naive-ui')['NInputNumber']
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']
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']
NUpload: typeof import('naive-ui')['NUpload']
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']
RomanNumeralConverter: typeof import('./src/tools/roman-numeral-converter/roman-numeral-converter.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default']
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
SearchBarItem: typeof import('./src/components/SearchBarItem.vue')['default']
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
SvgMeshGradientGenerator: typeof import('./src/tools/svg-mesh-gradient-generator/svg-mesh-gradient-generator.vue')['default']
SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default']
TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default']
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default']
TextToNatoAlphabet: typeof import('./src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue')['default']
TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default']
'TokenGenerator.tool': typeof import('./src/tools/token-generator/token-generator.tool.vue')['default']
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default']
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']
UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default']
UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
}
}

10
nginx.conf Normal file
View File

@@ -0,0 +1,10 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "it-tools",
"version": "2.15.0",
"version": "2023.4.23-92bd835",
"description": "Collection of handy online tools for developers, with great UX. ",
"keywords": [
"productivity",
@@ -25,78 +25,105 @@
"preview": "vite preview --port 5050",
"test": "npm run test:unit",
"test:unit": "vitest --environment jsdom",
"test:e2e": "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",
"release": "standard-version"
"script:create-new-tool": "node scripts/create-tool.mjs",
"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",
"@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",
"bcryptjs": "^2.4.3",
"change-case": "^4.1.2",
"colord": "^2.9.3",
"composerize-ts": "^0.6.2",
"country-code-lookup": "^0.0.23",
"cron-validator": "^1.3.1",
"cronstrue": "^2.15.0",
"cronstrue": "^2.26.0",
"crypto-js": "^4.1.1",
"date-fns": "^2.29.3",
"figue": "^1.2.0",
"fuse.js": "^6.6.2",
"highlight.js": "^11.6.0",
"json5": "^2.2.1",
"highlight.js": "^11.7.0",
"json5": "^2.2.3",
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.28",
"lodash": "^4.17.21",
"mathjs": "^10.6.4",
"mime-types": "^2.1.35",
"naive-ui": "^2.33.5",
"pinia": "^2.0.23",
"naive-ui": "^2.34.3",
"netmask": "^2.0.2",
"node-forge": "^1.3.1",
"oui": "^12.0.52",
"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",
"ua-parser-js": "^1.0.35",
"uuid": "^8.3.2",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
"vue": "^3.2.47",
"vue-router": "^4.1.6",
"yaml": "^2.2.1"
},
"devDependencies": {
"@playwright/test": "^1.32.3",
"@rushstack/eslint-patch": "^1.2.0",
"@types/bcryptjs": "^2.4.2",
"@types/crypto-js": "^4.1.1",
"@types/jsdom": "^16.2.15",
"@types/lodash": "^4.14.188",
"@types/lodash": "^4.14.192",
"@types/mime-types": "^2.1.1",
"@types/node": "^16.18.3",
"@types/netmask": "^2.0.0",
"@types/node": "^16.18.23",
"@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.42.1",
"@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.0.0",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^10.0.0",
"@vue/test-utils": "^2.2.2",
"@vue/test-utils": "^2.3.2",
"@vue/tsconfig": "^0.1.3",
"c8": "^7.12.0",
"eslint": "^8.27.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.26.0",
"c8": "^7.13.0",
"consola": "^3.0.2",
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-vue": "^8.7.1",
"jsdom": "^19.0.0",
"less": "^4.1.3",
"prettier": "^2.7.1",
"standard-version": "^9.5.0",
"start-server-and-test": "^1.14.0",
"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"
"workbox-window": "^6.5.4",
"zx": "^7.2.1"
}
}

82
playwright.config.ts Normal file
View File

@@ -0,0 +1,82 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './src',
testMatch: /.*\.e2e\.(spec\.)?ts/,
/* 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,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 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',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
testIdAttribute: 'data-test-id',
locale: 'en-GB',
timezoneId: 'Europe/Paris',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
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,
},
});

6232
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
const currentDirname = dirname(fileURLToPath(import.meta.url));
const toolsDir = join(currentDirname, '..', 'src', 'tools');
// eslint-disable-next-line no-undef
const toolName = process.argv[2];
if (!toolName) {
@@ -28,9 +29,9 @@ createToolFile(
`${toolName}.vue`,
`
<template>
<n-card>
<div>
Lorem ipsum
</n-card>
</div>
</template>
<script setup lang="ts">
@@ -55,6 +56,7 @@ export const tool = defineTool({
keywords: ['${toolName.split('-').join("', '")}'],
component: () => import('./${toolName}.vue'),
icon: ArrowsShuffle,
createdAt: new Date('${new Date().toISOString().split('T')[0]}'),
});
`,
);
@@ -72,6 +74,28 @@ import { expect, describe, it } from 'vitest';
`,
);
createToolFile(
`${toolName}.e2e.spec.ts`,
`
import { test, expect } from '@playwright/test';
test.describe('Tool - ${toolNameTitleCase}', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/${toolName}');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('${toolNameTitleCase} - IT Tools');
});
test('', async ({ page }) => {
});
});
`,
);
const toolsIndex = join(toolsDir, 'index.ts');
const indexContent = await readFile(toolsIndex, { encoding: 'utf-8' }).then((r) => r.split('\n'));

View File

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

57
scripts/release.mjs Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,134 @@
<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';
const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] });
const { toolsByCategory } = toRefs(props);
const route = useRoute();
const makeLabel = (tool: Tool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name });
const makeIcon = (tool: Tool) => () => h(MenuIconItem, { tool });
const collapsedCategories = useStorage<Record<string, boolean>>(
'menu-tool-option:collapsed-categories',
{},
undefined,
{
deep: true,
serializer: {
read: (v) => (v ? JSON.parse(v) : null),
write: (v) => JSON.stringify(v),
},
},
);
function toggleCategoryCollapse({ name }: { name: string }) {
collapsedCategories.value[name] = !collapsedCategories.value[name];
}
const menuOptions = computed(() =>
toolsByCategory.value.map(({ name, components }) => ({
name: name,
isCollapsed: collapsedCategories.value[name],
tools: components.map((tool) => ({
label: makeLabel(tool),
icon: makeIcon(tool),
key: tool.name,
})),
})),
);
const themeVars = useThemeVars();
</script>
<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;
.menu {
flex: 1;
margin-bottom: 5px;
::v-deep(.n-menu-item-content::before) {
left: 0;
right: 13px;
}
}
.toggle-bar {
width: 24px;
opacity: 0.1;
transition: opacity ease 0.2s;
position: relative;
cursor: pointer;
&::before {
width: 2px;
height: 100%;
content: ' ';
background-color: v-bind('themeVars.textColor3');
border-radius: 2px;
position: absolute;
top: 0;
left: 14px;
}
&:hover {
opacity: 0.5;
}
}
}
</style>

View File

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

View File

@@ -0,0 +1,42 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<c-button
variant="text"
circle
:type="buttonType"
:style="{ opacity: isFavorite ? 1 : 0.2 }"
@click="toggleFavorite"
>
<n-icon :component="FavoriteFilled" />
</c-button>
</template>
{{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }}
</n-tooltip>
</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 toolStore = useToolStore();
const props = defineProps<{ tool: Tool }>();
const { tool } = toRefs(props);
const isFavorite = computed(() => toolStore.isToolFavorite({ tool }));
const buttonType = computed(() => (isFavorite.value ? 'primary' : 'default'));
function toggleFavorite(event: MouseEvent) {
event.preventDefault();
if (toolStore.isToolFavorite({ tool })) {
toolStore.removeToolFromFavorites({ tool });
return;
}
toolStore.addToolToFavorites({ tool });
}
</script>

View File

@@ -0,0 +1,57 @@
<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';
const props = withDefaults(
defineProps<{
transformer?: (v: string) => string;
inputValidationRules?: UseValidationRule<string>[];
inputLabel?: string;
inputPlaceholder?: string;
inputDefault?: string;
outputLabel?: string;
outputLanguage?: string;
}>(),
{
transformer: _.identity,
inputValidationRules: () => [],
inputLabel: 'Input',
inputDefault: '',
inputPlaceholder: 'Input...',
outputLabel: 'Output',
outputLanguage: '',
},
);
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } =
toRefs(props);
const inputElement = ref();
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>

View File

@@ -3,9 +3,9 @@
<template #suffix>
<n-tooltip trigger="hover">
<template #trigger>
<n-button quaternary circle @click="onCopyClicked">
<c-button circle variant="text" @click="onCopyClicked">
<n-icon :component="ContentCopyFilled" />
</n-button>
</c-button>
</template>
{{ tooltipText }}
</n-tooltip>

View File

@@ -6,11 +6,11 @@
</template>
<script setup lang="ts">
import type { ITool } from '@/tools/tool';
import type { Tool } from '@/tools/tools.types';
import { useThemeVars } from 'naive-ui';
import { toRefs } from 'vue';
const props = defineProps<{ tool: ITool }>();
const props = defineProps<{ tool: Tool }>();
const { tool } = toRefs(props);
const theme = useThemeVars();

View File

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

View File

@@ -1,23 +1,24 @@
<script lang="ts" setup>
import { useFuzzySearch } from '@/composable/fuzzySearch';
import { useTracker } from '@/modules/tracker/tracker.services';
import { tools } from '@/tools';
import type { ITool } from '@/tools/tool';
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 { searchResult } = useFuzzySearch({
search: queryString,
data: tools,
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
});
const toolToOption = (tool: ITool) => ({ label: tool.name, value: tool.path, tool });
const inputEl = ref<HTMLElement>();
const displayDropDown = ref(true);
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
const options = computed(() => {
if (queryString.value === '') {
@@ -27,12 +28,11 @@ const options = computed(() => {
return searchResult.value.map(toolToOption);
});
function onSelect(path: string) {
router.push(path);
queryString.value = '';
}
const focusTarget = ref();
const { searchResult } = useFuzzySearch({
search: queryString,
data: tools,
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
});
const keys = useMagicKeys({
passive: false,
@@ -40,16 +40,40 @@ const keys = useMagicKeys({
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, () => {
focusTarget.value.focus();
});
whenever(keys.ctrl_k, claimFocus);
whenever(keys.meta_k, claimFocus);
whenever(keys.escape, releaseFocus);
function renderOption({ tool }: { tool: ITool }) {
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>
@@ -57,17 +81,19 @@ function renderOption({ tool }: { tool: ITool }) {
<n-auto-complete
v-model:value="queryString"
:options="options"
:on-select="(value) => onSelect(String(value))"
:on-select="(value: string | number) => onSelect(String(value))"
:render-label="renderOption"
:default-value="'aa'"
:get-show="() => true"
:get-show="() => displayDropDown"
:on-focus="onFocus"
@update:value="() => (displayDropDown = true)"
>
<template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
<n-input
ref="focusTarget"
ref="inputEl"
round
clearable
placeholder="Search a tool... [Ctrl + K]"
:placeholder="`Search a tool (use ${isMac ? 'Cmd' : 'Ctrl'} + K to focus)`"
:value="slotValue"
:input-props="{ autocomplete: 'disabled' }"
@input="handleInput"
@@ -82,5 +108,3 @@ function renderOption({ tool }: { tool: ITool }) {
</n-auto-complete>
</div>
</template>
<style lang="less" scoped></style>

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup>
import type { ITool } from '@/tools/tool';
import type { Tool } from '@/tools/tools.types';
import { toRefs } from 'vue';
const props = defineProps<{ tool: ITool }>();
const props = defineProps<{ tool: Tool }>();
const { tool } = toRefs(props);
</script>

View File

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

View File

@@ -1,28 +1,28 @@
<template>
<div style="overflow-x: hidden; width: 100%">
<n-card class="result-card">
<c-card class="result-card">
<n-scrollbar
x-scrollable
trigger="none"
:style="height ? `min-height: ${height - 40 /* card padding */ + 10 /* negative margin compensation */}px` : ''"
>
<n-config-provider :hljs="hljs">
<n-code :code="value" :language="language" :trim="false" />
<n-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 secondary circle size="large" @click="onCopyClicked">
<c-button circle important:h-10 important:w-10 @click="onCopyClicked">
<n-icon size="22" :component="Copy" />
</n-button>
</c-button>
</div>
</template>
<span>{{ tooltipText }}</span>
</n-tooltip>
</n-card>
<n-space v-if="copyPlacement === 'outside'" justify="center" style="margin-top: 15px">
<n-button secondary @click="onCopyClicked"> {{ tooltipText }} </n-button>
</c-card>
<n-space v-if="copyPlacement === 'outside'" justify="center" mt-4>
<c-button @click="onCopyClicked"> {{ tooltipText }} </c-button>
</n-space>
</div>
</template>
@@ -34,11 +34,13 @@ 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<{

View File

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

View File

@@ -0,0 +1,46 @@
import { computedAsync, watchThrottled } from '@vueuse/core';
import { computed, ref, watch } from 'vue';
export { computedRefreshable, computedRefreshableAsync };
function computedRefreshable<T>(getter: () => T, { throttle }: { throttle?: number } = {}) {
const dirty = ref(true);
let value: T;
const update = () => (dirty.value = true);
if (throttle) {
watchThrottled(getter, update, { throttle });
} else {
watch(getter, update);
}
const computedValue = computed(() => {
if (dirty.value) {
value = getter();
dirty.value = false;
}
return value;
});
return [computedValue, update] as const;
}
function computedRefreshableAsync<T>(getter: () => Promise<T>, defaultValue?: T) {
const dirty = ref(true);
let value: T;
const update = () => (dirty.value = true);
watch(getter, update);
const computedValue = computedAsync(async () => {
if (dirty.value) {
value = await getter();
dirty.value = false;
}
return value;
}, defaultValue);
return [computedValue, update] as const;
}

View File

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

View File

@@ -0,0 +1,35 @@
import { useRouteQuery } from '@vueuse/router';
import { computed } from 'vue';
export { useQueryParam };
const transformers = {
number: {
fromQuery: (value: string) => Number(value),
toQuery: (value: number) => String(value),
},
string: {
fromQuery: (value: string) => value,
toQuery: (value: string) => value,
},
boolean: {
fromQuery: (value: string) => value.toLowerCase() === 'true',
toQuery: (value: boolean) => (value ? 'true' : 'false'),
},
};
function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: T }) {
const type = typeof defaultValue;
const transformer = transformers[type as keyof typeof transformers] ?? transformers.string;
const proxy = useRouteQuery(name, transformer.toQuery(defaultValue as never));
return computed<T>({
get() {
return transformer.fromQuery(proxy.value) as unknown as T;
},
set(value) {
proxy.value = transformer.toQuery(value as never);
},
});
}

View File

@@ -3,7 +3,7 @@ import { reactive, watch, type Ref } from 'vue';
type ValidatorReturnType = unknown;
interface UseValidationRule<T> {
export interface UseValidationRule<T> {
validator: (value: T) => ValidatorReturnType;
message: string;
}
@@ -20,12 +20,20 @@ export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
}
}
type ValidationAttrs = {
export type ValidationAttrs = {
feedback: string;
validationStatus: string | undefined;
};
export function useValidation<T>({ source, rules }: { source: Ref<T>; rules: UseValidationRule<T>[] }) {
export function useValidation<T>({
source,
rules,
watch: watchRefs = [],
}: {
source: Ref<T>;
rules: UseValidationRule<T>[];
watch?: Ref<unknown>[];
}) {
const state = reactive<{
message: string;
status: undefined | 'error';
@@ -42,7 +50,7 @@ export function useValidation<T>({ source, rules }: { source: Ref<T>; rules: Use
});
watch(
[source],
[source, ...watchRefs],
() => {
state.message = '';
state.status = undefined;

View File

@@ -23,12 +23,18 @@ 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: {
isTrackerEnabled: {
doc: 'Is the tracker enabled',
format: 'boolean',
default: false,
env: 'VITE_TRACKER_ENABLED',
},
domain: {
doc: 'Plausible current domain',
format: 'string',
@@ -47,13 +53,11 @@ export const config = figue({
default: false,
},
},
tools: {
newTools: {
doc: 'Tool names for tools flagged a as new',
format: 'array',
default: [],
env: 'VITE_NEW_TOOLS',
},
showBanner: {
doc: 'Show the banner',
format: 'boolean',
default: false,
env: 'VITE_SHOW_BANNER',
},
})
.loadEnv({

View File

@@ -1,37 +1,33 @@
<script lang="ts" setup>
import { NIcon, useThemeVars, type MenuGroupOption } from 'naive-ui';
import { h } from 'vue';
import { RouterLink, useRoute } from 'vue-router';
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 { useStyleStore } from '@/stores/style.store';
import { config } from '@/config';
import MenuIconItem from '@/components/MenuIconItem.vue';
import type { ITool } from '@/tools/tool';
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 route = useRoute();
const styleStore = useStyleStore();
const version = config.app.version;
const commitSha = config.app.lastCommitSha.slice(0, 7);
const makeLabel = (tool: ITool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name });
const makeIcon = (tool: ITool) => () => h(MenuIconItem, { tool });
const { tracker } = useTracker();
const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
label: category.name,
key: category.name,
type: 'group',
children: category.components.map((tool) => ({
label: makeLabel(tool),
icon: makeIcon(tool),
key: tool.name,
})),
}));
const toolStore = useToolStore();
const tools = computed<ToolCategory[]>(() => [
...(toolStore.favoriteTools.length > 0 ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }] : []),
...toolsByCategory,
]);
</script>
<template>
@@ -51,51 +47,31 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
<navbar-buttons />
</n-space>
<n-menu
class="menu"
:value="(route.name as string)"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="menuOptions"
:indent="20"
/>
<collapsible-tool-menu :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">
Corentin Thomasset
</n-button>
<c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh"> Corentin Thomasset </c-link>
</div>
</div>
</div>
@@ -103,34 +79,24 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
<template #content>
<div class="navigation">
<n-button
<c-button
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
circle
quaternary
variant="text"
aria-label="Toggle menu"
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
>
<n-icon size="25" :component="Menu2" />
</n-button>
</c-button>
<router-link to="/" #="{ navigate, href }" custom>
<n-tooltip trigger="hover">
<template #trigger>
<n-button
tag="a"
:href="href"
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
circle
quaternary
aria-label="Home"
@click="navigate"
>
<n-icon size="25" :component="Home2" />
</n-button>
</template>
Home
</n-tooltip>
</router-link>
<n-tooltip trigger="hover">
<template #trigger>
<c-button to="/" circle variant="text" aria-label="Home">
<n-icon size="25" :component="Home2" />
</c-button>
</template>
Home
</n-tooltip>
<search-bar />
@@ -138,17 +104,18 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
<n-tooltip trigger="hover">
<template #trigger>
<n-button
<c-button
round
type="primary"
tag="a"
href="https://github.com/sponsors/CorentinTh"
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" style="margin-left: 5px" />
</n-button>
<n-icon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
</c-button>
</template>
Support IT Tools development !
</n-tooltip>
@@ -170,6 +137,19 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
// background-size: @size @size;
// }
.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 !important;
transition: padding ease 0.2s !important;
&:hover {
color: #fff;
padding-left: 30px;
padding-right: 30px;
}
}
.footer {
text-align: center;
color: #838587;

View File

@@ -3,22 +3,22 @@ import { useRoute } from 'vue-router';
import { useHead } from '@vueuse/head';
import type { HeadObject } from '@vueuse/head';
import { computed } from 'vue';
import { useThemeVars } from 'naive-ui';
import FavoriteButton from '@/components/FavoriteButton.vue';
import type { Tool } from '@/tools/tools.types';
import BaseLayout from './base.layout.vue';
const route = useRoute();
const theme = useThemeVars();
const head = computed<HeadObject>(() => ({
title: `${route.meta.name} - IT Tools`,
meta: [
{
name: 'description',
content: route.meta.description,
content: route.meta?.description as string,
},
{
name: 'keywords',
content: route.meta.keywords,
content: ((route.meta.keywords ?? []) as string[]).join(','),
},
],
}));
@@ -29,22 +29,18 @@ useHead(head);
<base-layout>
<div class="tool-layout">
<div class="tool-header">
<n-h1>
{{ route.meta.name }}
<n-space align="center" justify="space-between" :wrap="false">
<n-h1>
{{ route.meta.name }}
</n-h1>
<n-tag
v-if="route.meta.isNew"
round
type="success"
:bordered="false"
:color="{ color: theme.primaryColor, textColor: theme.tagColor }"
>
New tool
</n-tag>
<!-- <span class="new-tool-badge">New !</span> -->
</n-h1>
<div>
<favorite-button :tool="{name: route.meta.name} as Tool" />
</div>
</n-space>
<div class="separator" />
<div class="description">
{{ route.meta.description }}
</div>
@@ -92,6 +88,7 @@ useHead(head);
width: 200px;
height: 2px;
background: rgb(161, 161, 161);
opacity: 0.2;
margin: 10px 0;
}

View File

@@ -5,6 +5,8 @@ import { createHead } from '@vueuse/head';
import { registerSW } from 'virtual:pwa-register';
import { plausible } from './plugins/plausible.plugin';
import 'virtual:uno.css';
registerSW();
import { naive } from './plugins/naive.plugin';

View File

@@ -0,0 +1,27 @@
import _ from 'lodash';
import type Plausible from 'plausible-tracker';
import { inject } from 'vue';
export { createTrackerService, useTracker };
function createTrackerService({ plausible }: { plausible: ReturnType<typeof Plausible> }) {
return {
trackEvent({ eventName }: { eventName: string }) {
plausible.trackEvent(eventName);
},
};
}
function useTracker() {
const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible');
if (_.isNil(plausible)) {
throw new Error('Plausible must be instantiated');
}
const tracker = createTrackerService({ plausible });
return {
tracker,
};
}

View File

@@ -0,0 +1,3 @@
import type { createTrackerService } from './tracker.services';
export type TrackerService = ReturnType<typeof createTrackerService>;

View File

@@ -1,23 +1,18 @@
<script setup lang="ts">
import { Coffee } from '@vicons/tabler';
import { useHead } from '@vueuse/head';
useHead({ title: 'Page not found - IT Tools' });
</script>
<template>
<div class="e404-wrapper">
<n-result status="404" title="404 Not Found" description="Sorry, this page does not seem to exist">
<template #footer>
<router-link to="/" #="{ navigate, href }" custom>
<n-button tag="a" :href="href" secondary type="success" @click="navigate"> Back home </n-button>
</router-link>
</template>
</n-result>
<div mt-20 flex flex-col items-center>
<n-icon :component="Coffee" size="100" depth="3" />
<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>
<c-button to="/"> Back home </c-button>
</div>
</template>
<style scoped>
.e404-wrapper {
padding-top: 150px;
}
</style>

View File

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

View File

@@ -1,41 +1,72 @@
<script setup lang="ts">
import { toolsWithCategory } from '@/tools';
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';
const toolStore = useToolStore();
useHead({ title: 'IT Tools - Handy online tools for developers' });
</script>
<template>
<div class="home-page">
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
<n-gi>
<colored-card title="You like it-tools?" :icon="Heart">
Give us a star on
<a
href="https://github.com/CorentinTh/it-tools"
rel="noopener"
target="_blank"
aria-label="IT-Tools' github repository"
>github</a
>
or follow us on
<a
href="https://twitter.com/ittoolsdottech"
rel="noopener"
target="_blank"
aria-label="IT-Tools' twitter account"
>twitter</a
>! Thank you
<n-icon :component="Heart" />
</colored-card>
</n-gi>
<n-gi v-for="tool in toolsWithCategory" :key="tool.name">
<tool-card :tool="tool" />
</n-gi>
</n-grid>
<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
<a
href="https://github.com/CorentinTh/it-tools"
rel="noopener"
target="_blank"
aria-label="IT-Tools' GitHub repository"
>GitHub</a
>
or follow us on
<a
href="https://twitter.com/ittoolsdottech"
rel="noopener"
target="_blank"
aria-label="IT-Tools' Twitter account"
>Twitter</a
>! Thank you
<n-icon :component="Heart" />
</colored-card>
</n-gi>
</n-grid>
<transition name="height">
<div v-if="toolStore.favoriteTools.length > 0">
<n-h3>Your favorite tools</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" />
</n-gi>
</n-grid>
</div>
</transition>
<div v-if="toolStore.newTools.length > 0">
<n-h3>Newest tools</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" />
</n-gi>
</n-grid>
</div>
<n-h3>All the tools</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" />
</transition>
</n-gi>
</n-grid>
</div>
</div>
</template>
@@ -43,4 +74,27 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
.home-page {
padding-top: 50px;
}
.n-h3 {
margin-bottom: 10px;
}
::v-deep(.n-grid) {
margin-bottom: 30px;
}
.height-enter-active,
.height-leave-active {
transition: all 0.5s ease-in-out;
overflow: hidden;
max-height: 500px;
}
.height-enter-from,
.height-leave-to {
max-height: 42px;
overflow: hidden;
opacity: 0;
margin-bottom: 0;
}
</style>

View File

@@ -1,116 +1,3 @@
import {
create,
NAlert,
NAutoComplete,
NButton,
NCard,
NCode,
NCollapse,
NCollapseItem,
NCollapseTransition,
NColorPicker,
NConfigProvider,
NDatePicker,
NDivider,
NDropdown,
NDynamicInput,
NEllipsis,
NEmpty,
NForm,
NFormItem,
NGradientText,
NGrid,
NGridItem,
NH1,
NH2,
NH3,
NIcon,
NImage,
NInput,
NInputGroup,
NInputGroupLabel,
NInputNumber,
NLayout,
NLayoutSider,
NMenu,
NMessageProvider,
NModal,
NP,
NPageHeader,
NPopconfirm,
NProgress,
NResult,
NScrollbar,
NSelect,
NSlider,
NSpace,
NStatistic,
NSwitch,
NTable,
NTag,
NText,
NTimePicker,
NTooltip,
NUpload,
NUploadDragger,
NCheckbox,
} from 'naive-ui';
import { create } from 'naive-ui';
const components = [
NCheckbox,
NDynamicInput,
NDatePicker,
NCode,
NGradientText,
NScrollbar,
NImage,
NUploadDragger,
NTable,
NStatistic,
NDivider,
NInputGroup,
NInputGroupLabel,
NTag,
NResult,
NEllipsis,
NPageHeader,
NMessageProvider,
NLayout,
NLayoutSider,
NMenu,
NDropdown,
NH2,
NH3,
NP,
NAlert,
NTooltip,
NModal,
NEmpty,
NUpload,
NSelect,
NAutoComplete,
NProgress,
NCollapse,
NCollapseItem,
NSlider,
NPopconfirm,
NGrid,
NGridItem,
NButton,
NConfigProvider,
NCard,
NInput,
NColorPicker,
NInputNumber,
NSpace,
NH1,
NForm,
NFormItem,
NTimePicker,
NText,
NIcon,
NSwitch,
NCollapseTransition,
];
export const naive = create({ components });
export const naive = create();

View File

@@ -1,12 +1,38 @@
import { config } from '@/config';
import { noop } from 'lodash';
import Plausible from 'plausible-tracker';
import type { App } from 'vue';
function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> {
return {
trackEvent: noop,
enableAutoPageviews: () => noop,
};
}
function createPlausibleInstance({
config,
}: {
config: {
isTrackerEnabled: boolean;
domain: string;
apiHost: string;
trackLocalhost: boolean;
};
}) {
if (config.isTrackerEnabled) {
return Plausible(config);
}
return createFakePlausibleInstance();
}
export const plausible = {
install: (app: App) => {
const plausible = Plausible(config.plausible);
const plausible = createPlausibleInstance({ config: config.plausible });
plausible.enableAutoPageviews();
app.config.globalProperties.$plausible = plausible;
app.provide('plausible', plausible);
},
};

View File

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

View File

@@ -22,6 +22,10 @@ export const darkThemeOverrides: GlobalThemeOverrides = {
primaryColorSuppl: '#36AD6AFF',
},
Notification: {
color: '#333333',
},
AutoComplete: {
peers: {
InternalSelectMenu: { height: '500px', color: '#1e1e1e' },
@@ -33,18 +37,18 @@ export const darkThemeOverrides: GlobalThemeOverrides = {
},
Layout: {
color: '#121212',
siderColor: '#1e1e1e',
color: '#1c1c1c',
siderColor: '#232323',
siderBorderColor: 'transparent',
},
Card: {
color: '#1e1e1e',
borderColor: 'transparent',
color: '#232323',
borderColor: '#282828',
},
Table: {
tdColor: '#1e1e1e',
tdColor: '#232323',
thColor: '#353535',
},
};

View File

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

View File

@@ -1,5 +1,5 @@
<template>
<n-card title="String to base64">
<c-card title="String to base64">
<n-form-item label="String to encode">
<n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." rows="5" />
</n-form-item>
@@ -15,11 +15,11 @@
</n-form-item>
<n-space justify="center">
<n-button secondary @click="copyTextBase64()"> Copy base64 </n-button>
<c-button @click="copyTextBase64()"> Copy base64 </c-button>
</n-space>
</n-card>
</c-card>
<n-card title="Base64 to string">
<c-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>
@@ -29,9 +29,9 @@
</n-form-item>
<n-space justify="center">
<n-button secondary @click="copyText()"> Copy decoded string </n-button>
<c-button @click="copyText()"> Copy decoded string </c-button>
</n-space>
</n-card>
</c-card>
</template>
<script setup lang="ts">
@@ -53,5 +53,3 @@ const b64Validation = useValidation({
rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }],
});
</script>
<style lang="less" scoped></style>

View File

@@ -13,17 +13,15 @@
/>
</n-form-item>
<br />
<n-card>
<c-card>
<n-statistic label="Authorization header:" class="header">
<n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none">
{{ header }}
</n-scrollbar>
</n-statistic>
</n-card>
<br />
<n-space justify="center">
<n-button secondary @click="copy">Copy header</n-button>
</c-card>
<n-space justify="center" mt-5>
<c-button @click="copy">Copy header</c-button>
</n-space>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<n-card title="Hash">
<c-card title="Hash">
<n-form label-width="120">
<n-form-item label="Your string: " label-placement="left">
<n-input
@@ -12,17 +12,16 @@
/>
</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" style="width: 100%" />
<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 justify="center" mt-5>
<c-button @click="copy"> Copy hash </c-button>
</n-space>
</n-card>
</c-card>
<n-card title="Compare string with hash">
<c-card title="Compare string with hash">
<n-form label-width="120">
<n-form-item label="Your string: " label-placement="left">
<n-input
@@ -50,7 +49,7 @@
</div>
</n-form-item>
</n-form>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -0,0 +1,34 @@
import _ from 'lodash';
export { computeAverage, computeVariance, arrayToMarkdownTable };
function computeAverage({ data }: { data: number[] }) {
if (data.length === 0) {
return 0;
}
return _.sum(data) / data.length;
}
function computeVariance({ data }: { data: number[] }) {
const mean = computeAverage({ data });
const squaredDiffs = data.map((value) => Math.pow(value - mean, 2));
return computeAverage({ data: squaredDiffs });
}
function arrayToMarkdownTable({ data, headerMap = {} }: { data: 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 headerRow = `| ${headers.map((header) => headerMap[header] ?? header).join(' | ')} |`;
const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`;
const dataRows = rows.map((row) => `| ${row.join(' | ')} |`).join('\n');
return `${headerRow}\n${separatorRow}\n${dataRows}`;
}

View File

@@ -0,0 +1,158 @@
<template>
<n-scrollbar style="flex: 1" x-scrollable>
<n-space :wrap="false" style="flex: 1" justify="center" :size="0" mb-5>
<div v-for="(suite, index) of suites" :key="index">
<c-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>
</c-card>
<n-space 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>
</n-space>
</div>
</n-space>
</n-scrollbar>
<div style="flex: 0 0 100%">
<div style="max-width: 600px; margin: 0 auto">
<n-space justify="center">
<n-form-item label="Unit:" label-placement="left">
<n-input v-model:value="unit" placeholder="Unit (eg: ms)" />
</n-form-item>
<c-button
@click="
suites = [
{ title: 'Suite 1', data: [] },
{ title: 'Suite 2', data: [] },
]
"
>Reset suites</c-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>
<n-space justify="center" mt-5>
<c-button @click="copyAsMarkdown">Copy as markdown table</c-button>
<c-button @click="copyAsBulletList">Copy as bullet list</c-button>
</n-space>
</div>
</div>
</template>
<script setup lang="ts">
import { Trash, Plus } from '@vicons/tabler';
import { useClipboard, useStorage } from '@vueuse/core';
import _ from 'lodash';
import { computed } from 'vue';
import { computeAverage, computeVariance, arrayToMarkdownTable } from './benchmark-builder.models';
import DynamicValues from './dynamic-values.vue';
const suites = useStorage('benchmark-builder:suites', [
{ title: 'Suite 1', data: [5, 10] },
{ title: 'Suite 2', data: [8, 12] },
]);
const unit = useStorage('benchmark-builder:unit', '');
const round = (v: number) => Math.round(v * 1000) / 1000;
const results = computed(() => {
return suites.value
.map(({ data: dirtyData, title }) => {
const data = dirtyData.filter(_.isNumber);
return {
title,
size: data.length,
mean: computeAverage({ data }),
variance: computeVariance({ data }),
};
})
.sort((a, b) => a.mean - b.mean)
.map(({ mean, variance, size, title }, index, suites) => {
const cleanUnit = unit.value.trim();
const bestMean: number = suites[0].mean;
const deltaWithBestMean = mean - bestMean;
const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean);
const comparisonValues: string =
index !== 0 && bestMean !== mean ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
return {
position: index + 1,
title,
mean: `${round(mean)}${cleanUnit}${comparisonValues}`,
variance: `${round(variance)}${cleanUnit}${cleanUnit ? '²' : ''}`,
size,
};
});
});
const { copy } = useClipboard();
const header = {
title: 'Suite',
size: 'Samples',
mean: 'Mean',
variance: 'Variance',
position: 'Position',
};
function copyAsMarkdown() {
copy(arrayToMarkdownTable({ data: results.value, headerMap: header }));
}
function copyAsBulletList() {
const bulletList = results.value
.flatMap(({ title, ...sections }) => {
return [
` - ${title}`,
...Object.entries(sections).map(
([key, value]) => ` - ${header[key as keyof typeof header] ?? key}: ${value}`,
),
];
})
.join('\n');
copy(bulletList);
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,57 @@
<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>
<c-button circle variant="text" @click="values.splice(index, 1)">
<n-icon :component="Trash" depth="3" size="18" />
</c-button>
</template>
Delete value
</n-tooltip>
</n-space>
<c-button @click="addValue">
<n-icon :component="Plus" depth="3" mr-2 size="18" />
Add a measure
</c-button>
</div>
</template>
<script setup lang="ts">
import { Trash, Plus } from '@vicons/tabler';
import { useTemplateRefsList, useVModel } from '@vueuse/core';
import { NInputNumber } from 'naive-ui';
import { nextTick } from 'vue';
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() {
values.value.push(null);
await nextTick();
refs.value.at(-1)?.focus();
}
function onInputEnter(index: number) {
if (index === values.value.length - 1) {
addValue();
return;
}
refs.value.at(index + 1)?.focus();
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,12 @@
import { SpeedFilled } from '@vicons/material';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Benchmark builder',
path: '/benchmark-builder',
description: 'Easily compare execution time of tasks with this very simple online benchmark builder.',
keywords: ['benchmark', 'builder', 'execution', 'duration', 'mean', 'variance'],
component: () => import('./benchmark-builder.vue'),
icon: SpeedFilled,
createdAt: new Date('2023-04-05'),
});

View File

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

View File

@@ -1,5 +1,5 @@
<template>
<n-card>
<c-card>
<n-form label-width="120" label-placement="left" :show-feedback="false">
<n-form-item label="Your string:">
<n-input v-model:value="input" />
@@ -8,40 +8,40 @@
<n-divider />
<n-form-item label="Camelcase:">
<input-copyable :value="camelCase(input)" />
<input-copyable :value="camelCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Capitalcase:">
<input-copyable :value="capitalCase(input)" />
<input-copyable :value="capitalCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Constantcase:">
<input-copyable :value="constantCase(input)" />
<input-copyable :value="constantCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Dotcase:">
<input-copyable :value="dotCase(input)" />
<input-copyable :value="dotCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Headercase:">
<input-copyable :value="headerCase(input)" />
<input-copyable :value="headerCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Nocase:">
<input-copyable :value="noCase(input)" />
<input-copyable :value="noCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Paramcase:">
<input-copyable :value="paramCase(input)" />
<input-copyable :value="paramCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Pascalcase:">
<input-copyable :value="pascalCase(input)" />
<input-copyable :value="pascalCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Pathcase:">
<input-copyable :value="pathCase(input)" />
<input-copyable :value="pathCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Sentencecase:">
<input-copyable :value="sentenceCase(input)" />
<input-copyable :value="sentenceCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Snakecase:">
<input-copyable :value="snakeCase(input)" />
<input-copyable :value="snakeCase(input, baseConfig)" />
</n-form-item>
</n-form>
</n-card>
</c-card>
</template>
<script setup lang="ts">
@@ -61,6 +61,10 @@ import {
} from 'change-case';
import InputCopyable from '../../components/InputCopyable.vue';
const baseConfig = {
stripRegexp: /[^A-Za-zÀ-ÖØ-öø-ÿ]+/gi,
};
const input = ref('lorem ipsum dolor sit amet');
</script>

View File

@@ -24,7 +24,7 @@
{{ octal }}
</div>
<input-copyable :value="`chmod ${octal} path`" readonly style="margin-bottom: 5px" />
<input-copyable :value="`chmod ${octal} path`" readonly />
</div>
</template>

View File

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

View File

@@ -1,5 +1,5 @@
<template>
<n-card>
<c-card>
<n-form label-width="100" label-placement="left">
<n-form-item label="color picker:">
<n-color-picker
@@ -30,7 +30,7 @@
<input-copyable v-model:value="cmyk" :on-input="(v: string) => onInputUpdated(v, 'cmyk')" />
</n-form-item>
</n-form>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -1,5 +1,5 @@
<template>
<n-card>
<c-card>
<n-form-item
class="cron"
:show-label="false"
@@ -27,8 +27,8 @@
</n-form-item>
</n-form>
</n-space>
</n-card>
<n-card>
</c-card>
<c-card>
<pre>
[optional] seconds (0 - 59)
| minute (0 - 59)
@@ -41,7 +41,7 @@
>
<n-space v-if="styleStore.isSmallScreen" vertical>
<n-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" embedded :bordered="false">
<c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" important:border-none>
<div>
Symbol: <strong>{{ symbol }}</strong>
</div>
@@ -57,7 +57,7 @@
<div>
Equivalent: <strong>{{ equivalent }}</strong>
</div>
</n-card>
</c-card>
</n-space>
<n-table v-else size="small">
<thead>
@@ -79,7 +79,7 @@
</tr>
</tbody>
</n-table>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

@@ -0,0 +1,33 @@
import { test, expect } from '@playwright/test';
test.describe('Date time converter - json to yaml', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/date-converter');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('Date-time converter - IT Tools');
});
test('Format is auto detected from a date and the date is correctly converted', async ({ page }) => {
const initialFormat = await page.getByTestId('date-time-converter-format-select').innerText();
expect(initialFormat.trim()).toEqual('Timestamp');
await page.getByTestId('date-time-converter-input').fill('2023-04-12T23:10:24+02:00');
const detectedFormat = await page.getByTestId('date-time-converter-format-select').innerText();
expect(detectedFormat.trim()).toEqual('ISO 8601');
expect((await page.getByTestId('JS locale date string').inputValue()).trim()).toEqual(
'Wed Apr 12 2023 23:10:24 GMT+0200 (Central European Summer Time)',
);
expect((await page.getByTestId('ISO 8601').inputValue()).trim()).toEqual('2023-04-12T23:10:24+02:00');
expect((await page.getByTestId('ISO 9075').inputValue()).trim()).toEqual('2023-04-12 23:10:24');
expect((await page.getByTestId('Unix timestamp').inputValue()).trim()).toEqual('1681333824');
expect((await page.getByTestId('RFC 7231').inputValue()).trim()).toEqual('Wed, 12 Apr 2023 21:10:24 GMT');
expect((await page.getByTestId('RFC 3339').inputValue()).trim()).toEqual('2023-04-12T23:10:24+02:00');
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');
});
});

View File

@@ -0,0 +1,142 @@
import { describe, test, expect } from 'vitest';
import {
isISO8601DateTimeString,
isISO9075DateString,
isRFC3339DateString,
isRFC7231DateString,
isUnixTimestamp,
isTimestamp,
isUTCDateString,
isMongoObjectId,
} from './date-time-converter.models';
describe('date-time-converter models', () => {
describe('isISO8601DateTimeString', () => {
test('should return true for valid ISO 8601 date strings', () => {
expect(isISO8601DateTimeString('2021-01-01T00:00:00.000Z')).toBe(true);
expect(isISO8601DateTimeString('2023-04-12T14:56:00+01:00')).toBe(true);
expect(isISO8601DateTimeString('20230412T145600+0100')).toBe(true);
expect(isISO8601DateTimeString('20230412T145600Z')).toBe(true);
expect(isISO8601DateTimeString('2016-02-01')).toBe(true);
expect(isISO8601DateTimeString('2016')).toBe(true);
});
test('should return false for invalid ISO 8601 date strings', () => {
expect(isISO8601DateTimeString()).toBe(false);
expect(isISO8601DateTimeString('')).toBe(false);
expect(isISO8601DateTimeString('qsdqsd')).toBe(false);
expect(isISO8601DateTimeString('2016-02-01-')).toBe(false);
expect(isISO8601DateTimeString('2021-01-01T00:00:00.')).toBe(false);
});
});
describe('isISO9075DateString', () => {
test('should return true for valid ISO 9075 date strings', () => {
expect(isISO9075DateString('2022-01-01 12:00:00Z')).toBe(true);
expect(isISO9075DateString('2022-01-01 12:00:00.123456Z')).toBe(true);
expect(isISO9075DateString('2022-01-01 12:00:00+01:00')).toBe(true);
expect(isISO9075DateString('2022-01-01 12:00:00-05:00')).toBe(true);
});
test('should return false for invalid ISO 9075 date strings', () => {
expect(isISO9075DateString('2022/01/01T12:00:00Z')).toBe(false);
expect(isISO9075DateString('2022-01-01 12:00:00.123456789Z')).toBe(false);
expect(isISO9075DateString('2022-01-01 12:00:00+1:00')).toBe(false);
expect(isISO9075DateString('2022-01-01 12:00:00-05:')).toBe(false);
expect(isISO9075DateString('2022-01-01 12:00:00-05:00:00')).toBe(false);
expect(isISO9075DateString('2022-01-01')).toBe(false);
expect(isISO9075DateString('12:00:00Z')).toBe(false);
expect(isISO9075DateString('2022-01-01T12:00:00Zfoo')).toBe(false);
});
});
describe('isRFC3339DateString', () => {
test('should return true for valid RFC 3339 date strings', () => {
expect(isRFC3339DateString('2022-01-01T12:00:00Z')).toBe(true);
expect(isRFC3339DateString('2022-01-01T12:00:00.123456789Z')).toBe(true);
expect(isRFC3339DateString('2022-01-01T12:00:00.123456789+01:00')).toBe(true);
expect(isRFC3339DateString('2022-01-01T12:00:00-05:00')).toBe(true);
});
test('should return false for invalid RFC 3339 date strings', () => {
expect(isRFC3339DateString('2022/01/01T12:00:00Z')).toBe(false);
expect(isRFC3339DateString('2022-01-01T12:00:00.123456789+1:00')).toBe(false);
expect(isRFC3339DateString('2022-01-01T12:00:00-05:')).toBe(false);
expect(isRFC3339DateString('2022-01-01T12:00:00-05:00:00')).toBe(false);
expect(isRFC3339DateString('2022-01-01')).toBe(false);
expect(isRFC3339DateString('12:00:00Z')).toBe(false);
expect(isRFC3339DateString('2022-01-01T12:00:00Zfoo')).toBe(false);
});
});
describe('isRFC7231DateString', () => {
test('should return true for valid RFC 7231 date strings', () => {
expect(isRFC7231DateString('Sun, 06 Nov 1994 08:49:37 GMT')).toBe(true);
expect(isRFC7231DateString('Tue, 22 Apr 2014 07:00:00 GMT')).toBe(true);
});
test('should return false for invalid RFC 7231 date strings', () => {
expect(isRFC7231DateString('06 Nov 1994 08:49:37 GMT')).toBe(false);
expect(isRFC7231DateString('Sun, 06 Nov 94 08:49:37 GMT')).toBe(false);
expect(isRFC7231DateString('Sun, 06 Nov 1994 8:49:37 GMT')).toBe(false);
expect(isRFC7231DateString('Sun, 06 Nov 1994 08:49:37 GMT-0500')).toBe(false);
expect(isRFC7231DateString('Sun, 06 November 1994 08:49:37 GMT')).toBe(false);
expect(isRFC7231DateString('Sunday, 06 Nov 1994 08:49:37 GMT')).toBe(false);
expect(isRFC7231DateString('06 Nov 1994')).toBe(false);
});
});
describe('isUnixTimestamp', () => {
test('should return true for valid Unix timestamps', () => {
expect(isUnixTimestamp('1649789394')).toBe(true);
expect(isUnixTimestamp('1234567890')).toBe(true);
expect(isUnixTimestamp('0')).toBe(true);
});
test('should return false for invalid Unix timestamps', () => {
expect(isUnixTimestamp('foo')).toBe(false);
expect(isUnixTimestamp('')).toBe(false);
});
});
describe('isTimestamp', () => {
test('should return true for valid Unix timestamps in milliseconds', () => {
expect(isTimestamp('1649792026123')).toBe(true);
expect(isTimestamp('1234567890000')).toBe(true);
expect(isTimestamp('0')).toBe(true);
});
test('should return false for invalid Unix timestamps in milliseconds', () => {
expect(isTimestamp('foo')).toBe(false);
expect(isTimestamp('')).toBe(false);
});
});
describe('isUTCDateString', () => {
test('should return true for valid UTC date strings', () => {
expect(isUTCDateString('Sun, 06 Nov 1994 08:49:37 GMT')).toBe(true);
expect(isUTCDateString('Tue, 22 Apr 2014 07:00:00 GMT')).toBe(true);
});
test('should return false for invalid UTC date strings', () => {
expect(isUTCDateString('06 Nov 1994 08:49:37 GMT')).toBe(false);
expect(isUTCDateString('16497920261')).toBe(false);
expect(isUTCDateString('foo')).toBe(false);
expect(isUTCDateString('')).toBe(false);
});
});
describe('isMongoObjectId', () => {
test('should return true for valid Mongo ObjectIds', () => {
expect(isMongoObjectId('507f1f77bcf86cd799439011')).toBe(true);
expect(isMongoObjectId('507f1f77bcf86cd799439012')).toBe(true);
});
test('should return false for invalid Mongo ObjectIds', () => {
expect(isMongoObjectId('507f1f77bcf86cd79943901')).toBe(false);
expect(isMongoObjectId('507f1f77bcf86cd79943901z')).toBe(false);
expect(isMongoObjectId('foo')).toBe(false);
expect(isMongoObjectId('')).toBe(false);
});
});
});

View File

@@ -0,0 +1,46 @@
import _ from 'lodash';
export {
isISO8601DateTimeString,
isISO9075DateString,
isRFC3339DateString,
isRFC7231DateString,
isUnixTimestamp,
isTimestamp,
isUTCDateString,
isMongoObjectId,
};
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 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$/;
function createRegexMatcher(regex: RegExp) {
return (date?: string) => !_.isNil(date) && regex.test(date);
}
const isISO8601DateTimeString = createRegexMatcher(ISO8601_REGEX);
const isISO9075DateString = createRegexMatcher(ISO9075_REGEX);
const isRFC3339DateString = createRegexMatcher(RFC3339_REGEX);
const isRFC7231DateString = createRegexMatcher(RFC7231_REGEX);
const isUnixTimestamp = createRegexMatcher(/^[0-9]{1,10}$/);
const isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/);
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
function isUTCDateString(date?: string) {
if (_.isNil(date)) {
return false;
}
try {
return new Date(date).toUTCString() === date;
} catch (_ignored) {
return false;
}
}

View File

@@ -0,0 +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;
};

View File

@@ -1,44 +1,39 @@
<template>
<div>
<n-card>
<n-space justify="center">
<n-form-item label="Use current date-time ?" label-placement="left" :show-feedback="false">
<n-switch v-model:value="useCurrentDate" />
</n-form-item>
</n-space>
<n-form-item
:feedback="inputInvalid ? 'Invalid date for the current format' : ''"
:validation-status="inputInvalid ? 'error' : undefined"
>
<n-input-group style="flex-grow: 1">
<n-select
v-model:value="inputFormat"
style="width: 200px"
:options="formats.map(({ name }, i) => ({ label: name, value: i }))"
:disabled="useCurrentDate"
/>
<n-form-item :show-label="false" v-bind="validation.attrs">
<n-input-group>
<n-input
v-model:value="inputDate"
autofocus
:on-input="onDateInputChanged"
placeholder="Put you date string here..."
clearable
:input-props="{ 'data-test-id': 'date-time-converter-input' }"
/>
<n-input
v-model:value="inputDate"
:on-input="onDateInputChanged"
:disabled="useCurrentDate"
placeholder="Your date string..."
/>
</n-input-group>
</n-form-item>
<n-divider style="margin-top: 0" />
<div v-for="{ name, fromDate } in formats" :key="name" style="margin: 5px 0">
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> {{ name }}: </n-input-group-label>
<input-copyable :value="fromDate(baseDate)" />
</n-input-group>
</div>
</n-card>
<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 { useRafFn } from '@vueuse/core';
import {
formatISO,
formatISO9075,
@@ -47,95 +42,132 @@ import {
fromUnixTime,
getTime,
getUnixTime,
isDate,
parseISO,
parseJSON,
isDate,
isValid,
} from 'date-fns';
import { ref } from 'vue';
import InputCopyable from '../../components/InputCopyable.vue';
import { withDefaultOnError } from '@/utils/defaults';
import { useValidation } from '@/composable/validation';
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
import {
isISO8601DateTimeString,
isISO9075DateString,
isRFC3339DateString,
isRFC7231DateString,
isTimestamp,
isUTCDateString,
isUnixTimestamp,
isMongoObjectId,
} from './date-time-converter.models';
const useCurrentDate = ref(true);
const inputDate = ref('');
const inputFormat = ref(6);
const inputInvalid = ref(false);
const baseDate = ref(new Date());
useRafFn(() => {
if (useCurrentDate.value) {
baseDate.value = new Date();
const toDate: ToDateMapper = (date) => new Date(date);
function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) {
if (!date || !validation.isValid) {
return '';
}
});
function onDateInputChanged(value: string) {
const { toDate } = formats[inputFormat.value];
inputInvalid.value = false;
try {
const formatted: Date | string = toDate(value);
if (!isDate(formatted) || isNaN(formatted.getTime())) {
throw new Error('Invalid date');
}
baseDate.value = formatted;
} catch (_) {
inputInvalid.value = true;
}
return withDefaultOnError(() => formatter(date), '');
}
type Format = {
name: string;
fromDate: (date: Date) => string;
toDate: (value: string) => Date;
};
const toDate: Format['toDate'] = (date) => new Date(date);
const formats: Format[] = [
const formats: DateFormat[] = [
{
name: 'JS locale date string',
fromDate: (date) => date.toString(),
toDate,
formatMatcher: () => false,
},
{
name: 'ISO 8601',
fromDate: formatISO,
toDate: parseISO,
formatMatcher: (date) => isISO8601DateTimeString(date),
},
{
name: 'ISO 9075',
fromDate: formatISO9075,
toDate: parseISO,
formatMatcher: (date) => isISO9075DateString(date),
},
{
name: 'RFC 3339',
fromDate: formatRFC3339,
toDate,
formatMatcher: (date) => isRFC3339DateString(date),
},
{
name: 'RFC 7231',
fromDate: formatRFC7231,
toDate,
},
{
name: 'Timestamp',
fromDate: (date) => String(getTime(date)),
toDate: (ms) => parseJSON(+ms),
formatMatcher: (date) => isRFC7231DateString(date),
},
{
name: 'Unix timestamp',
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),
},
{
name: 'UTC format',
fromDate: (date) => date.toUTCString(),
toDate,
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),
},
];
const formatIndex = ref(6);
const now = useNow();
const normalizedDate = computed(() => {
if (!inputDate.value) {
return now.value;
}
const { toDate } = formats[formatIndex.value];
try {
return toDate(inputDate.value);
} catch (_ignored) {
return undefined;
}
});
function onDateInputChanged(value: string) {
const matchingIndex = formats.findIndex(({ formatMatcher }) => formatMatcher(value));
if (matchingIndex !== -1) {
formatIndex.value = matchingIndex;
}
}
const validation = useValidation({
source: inputDate,
watch: [formatIndex],
rules: [
{
message: 'This date is invalid for this format',
validator: (value) =>
withDefaultOnError(() => {
if (value === '') return true;
const maybeDate = formats[formatIndex.value].toDate(value);
return isDate(maybeDate) && isValid(maybeDate);
}, false),
},
],
});
</script>

View File

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

View File

@@ -0,0 +1,4 @@
declare module 'composerize' {
const composerize: (arg: string) => string;
export default composerize;
}

View File

@@ -0,0 +1,81 @@
<template>
<div>
<n-form-item label="Your docker run command:" :show-feedback="false">
<n-input
v-model:value="dockerRun"
style="font-family: monospace"
type="textarea"
placeholder="Your docker run command to convert..."
rows="3"
/>
</n-form-item>
<n-divider />
<textarea-copyable :value="dockerCompose" language="yaml" />
<n-space justify="center" mt-5>
<c-button :disabled="dockerCompose === ''" secondary @click="download"> Download docker-compose.yml </c-button>
</n-space>
<div v-if="notComposable.length > 0">
<n-alert title="This options are not translatable to docker-compose" type="info" mt-5>
<ul>
<li v-for="(message, index) of notComposable" :key="index">{{ message }}</li>
</ul>
</n-alert>
</div>
<div v-if="notImplemented.length > 0">
<n-alert
title="This options are not yet implemented and therefore haven't been translated to docker-compose"
type="warning"
mt-5
>
<ul>
<li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li>
</ul>
</n-alert>
</div>
<div v-if="errors.length > 0">
<n-alert title="The following errors occured" type="error" mt-5>
<ul>
<li v-for="(message, index) of errors" :key="index">{{ message }}</li>
</ul>
</n-alert>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { withDefaultOnError } from '@/utils/defaults';
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
import { textToBase64 } from '@/utils/base64';
import TextareaCopyable from '@/components/TextareaCopyable.vue';
import { composerize, MessageType } from 'composerize-ts';
const dockerRun = ref(
'docker run -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro --restart always --log-opt max-size=1g nginx',
);
const conversionResult = computed(() =>
withDefaultOnError(() => composerize(dockerRun.value), { yaml: '', messages: [] }),
);
const dockerCompose = computed(() => conversionResult.value.yaml);
const notImplemented = computed(() =>
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notImplemented).map((msg) => msg.value),
);
const notComposable = computed(() =>
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notTranslatable).map((msg) => msg.value),
);
const errors = computed(() =>
conversionResult.value.messages
.filter((msg) => msg.type === MessageType.errorDuringConversion)
.map((msg) => msg.value),
);
const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value));
const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' });
</script>

View File

@@ -0,0 +1,11 @@
import { BrandDocker } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Docker run to Docker compose converter',
path: '/docker-run-to-docker-compose-converter',
description: 'Turns docker run commands into docker-compose files!',
keywords: ['docker', 'run', 'compose', 'yaml', 'yml', 'convert', 'deamon'],
component: () => import('./docker-run-to-docker-compose-converter.vue'),
icon: BrandDocker,
});

View File

@@ -1,5 +1,5 @@
<template>
<n-card title="Encrypt">
<c-card title="Encrypt">
<n-space item-style="flex: 1 1 0">
<n-form-item label="Your text:" :show-feedback="false">
<n-input
@@ -21,8 +21,7 @@
</n-form-item>
</n-space>
</n-space>
<br />
<n-form-item label="Your text encrypted:" :show-feedback="false">
<n-form-item label="Your text encrypted:" :show-feedback="false" mt-5>
<n-input
:value="cypherOutput"
type="textarea"
@@ -35,8 +34,8 @@
spellcheck="false"
/>
</n-form-item>
</n-card>
<n-card title="Decrypt">
</c-card>
<c-card title="Decrypt">
<n-space item-style="flex: 1 1 0">
<n-form-item label="Your encrypted text:" :show-feedback="false">
<n-input
@@ -58,8 +57,7 @@
</n-form-item>
</n-space>
</n-space>
<br />
<n-form-item label="Your decrypted text:" :show-feedback="false">
<n-form-item label="Your decrypted text:" :show-feedback="false" mt-5>
<n-input
:value="decryptOutput"
type="textarea"
@@ -72,7 +70,7 @@
spellcheck="false"
/>
</n-form-item>
</n-card>
</c-card>
</template>
<script setup lang="ts">

View File

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

View File

@@ -1,6 +1,6 @@
<template>
<div>
<n-card>
<c-card>
<n-input v-model:value="clearText" type="textarea" placeholder="Your string to hash..." rows="3" />
<n-divider />
@@ -35,11 +35,12 @@
<input-copyable :value="hashText(algo, clearText)" readonly />
</n-input-group>
</div>
</n-card>
</c-card>
</div>
</template>
<script setup lang="ts">
import { useQueryParam } from '@/composable/queryParams';
import { enc, lib, MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512 } from 'crypto-js';
import { ref } from 'vue';
import InputCopyable from '../../components/InputCopyable.vue';
@@ -59,7 +60,7 @@ const algos = {
type AlgoNames = keyof typeof algos;
type Encoding = keyof typeof enc | 'Bin';
const algoNames = Object.keys(algos) as AlgoNames[];
const encoding = ref<Encoding>('Hex');
const encoding = useQueryParam<Encoding>({ defaultValue: 'Hex', name: 'encoding' });
const clearText = ref('');
function formatWithEncoding(words: lib.WordArray, encoding: Encoding) {

View File

@@ -43,7 +43,7 @@
<n-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." />
</n-form-item>
<n-space justify="center">
<n-button secondary @click="copy()">Copy HMAC</n-button>
<c-button @click="copy()">Copy HMAC</c-button>
</n-space>
</div>
</template>
@@ -94,5 +94,3 @@ const hmac = computed(() =>
);
const { copy } = useCopy({ source: hmac });
</script>
<style lang="less" scoped></style>

View File

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

View File

@@ -0,0 +1,131 @@
<template>
<c-card v-if="editor" important:p0>
<menu-bar class="editor-header" :editor="editor" />
<n-divider style="margin-top: 0" />
<div px8 pb6>
<editor-content class="editor-content" :editor="editor" />
</div>
</c-card>
</template>
<script setup lang="ts">
import { tryOnBeforeUnmount, useVModel } from '@vueuse/core';
import { Editor, EditorContent } from '@tiptap/vue-3';
import StarterKit from '@tiptap/starter-kit';
import { useThemeVars } from 'naive-ui';
import MenuBar from './menu-bar.vue';
const themeVars = useThemeVars();
const props = defineProps<{ html: string }>();
const emit = defineEmits(['update:html']);
const html = useVModel(props, 'html', emit);
const editor = new Editor({
content: html.value,
extensions: [StarterKit],
});
editor.on('update', ({ editor }) => emit('update:html', editor.getHTML()));
tryOnBeforeUnmount(() => {
editor.destroy();
});
</script>
<style scoped lang="less">
::v-deep(.ProseMirror-focused) {
outline: none;
}
</style>
<style scoped lang="less">
::v-deep(.ProseMirror) {
> * + * {
margin-top: 0.75em;
}
p {
margin: 0;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: v-bind('themeVars.codeColor');
padding: 2px 4px;
border-radius: 5px;
font-size: 85%;
}
pre {
background: v-bind('themeVars.codeColor');
font-family: monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
mark {
background-color: #faf594;
}
img {
max-width: 100%;
height: auto;
}
hr {
margin: 1rem 0;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0d0d0d, 0.1);
}
hr {
border: none;
border-top: 2px solid rgba(#0d0d0d, 0.1);
margin: 2rem 0;
}
ul[data-type='taskList'] {
list-style: none;
padding: 0;
li {
display: flex;
align-items: center;
> label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
}
> div {
flex: 1 1 auto;
}
}
}
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<c-button circle variant="text" :type="isActive?.() ? 'primary' : 'default'" @click="action">
<n-icon :component="icon" />
</c-button>
</template>
{{ title }}
</n-tooltip>
</template>
<script setup lang="ts">
import { toRefs, type Component } from 'vue';
const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean }>();
const { icon, title, action, isActive } = toRefs(props);
</script>
<style scoped></style>

View File

@@ -0,0 +1,169 @@
<template>
<n-space align="center" :size="0">
<template v-for="(item, index) in items">
<n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical />
<menu-bar-item v-else-if="item.type === 'button'" :key="index" v-bind="item" />
</template>
</n-space>
</template>
<script setup lang="ts">
import type { Editor } from '@tiptap/vue-3';
import {
ArrowBack,
ArrowForwardUp,
Blockquote,
Bold,
ClearFormatting,
Code,
CodePlus,
H1,
H2,
H3,
H4,
Italic,
List,
ListNumbers,
Strikethrough,
TextWrap,
} from '@vicons/tabler';
import { toRefs, type Component } from 'vue';
import MenuBarItem from './menu-bar-item.vue';
const props = defineProps<{ editor: Editor }>();
const { editor } = toRefs(props);
type MenuItem =
| {
icon: Component;
title: string;
action: () => void;
isActive?: () => boolean;
type: 'button';
}
| { type: 'divider' };
const items: MenuItem[] = [
{
type: 'button',
icon: Bold,
title: 'Bold',
action: () => editor.value.chain().focus().toggleBold().run(),
isActive: () => editor.value.isActive('bold'),
},
{
type: 'button',
icon: Italic,
title: 'Italic',
action: () => editor.value.chain().focus().toggleItalic().run(),
isActive: () => editor.value.isActive('italic'),
},
{
type: 'button',
icon: Strikethrough,
title: 'Strike',
action: () => editor.value.chain().focus().toggleStrike().run(),
isActive: () => editor.value.isActive('strike'),
},
{
type: 'button',
icon: Code,
title: 'Inline code',
action: () => editor.value.chain().focus().toggleCode().run(),
isActive: () => editor.value.isActive('code'),
},
{
type: 'divider',
},
{
type: 'button',
icon: H1,
title: 'Heading 1',
action: () => editor.value.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: () => editor.value.isActive('heading', { level: 1 }),
},
{
type: 'button',
icon: H2,
title: 'Heading 2',
action: () => editor.value.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: () => editor.value.isActive('heading', { level: 2 }),
},
{
type: 'button',
icon: H3,
title: 'Heading 3',
action: () => editor.value.chain().focus().toggleHeading({ level: 4 }).run(),
isActive: () => editor.value.isActive('heading', { level: 4 }),
},
{
type: 'button',
icon: H4,
title: 'Heading 4',
action: () => editor.value.chain().focus().toggleHeading({ level: 4 }).run(),
isActive: () => editor.value.isActive('heading', { level: 4 }),
},
{
type: 'divider',
},
{
type: 'button',
icon: List,
title: 'Bullet list',
action: () => editor.value.chain().focus().toggleBulletList().run(),
isActive: () => editor.value.isActive('bulletList'),
},
{
type: 'button',
icon: ListNumbers,
title: 'Ordered list',
action: () => editor.value.chain().focus().toggleOrderedList().run(),
isActive: () => editor.value.isActive('orderedList'),
},
{
type: 'button',
icon: CodePlus,
title: 'Code block',
action: () => editor.value.chain().focus().toggleCodeBlock().run(),
isActive: () => editor.value.isActive('codeBlock'),
},
{
type: 'button',
icon: Blockquote,
title: 'Blockquote',
action: () => editor.value.chain().focus().toggleBlockquote().run(),
isActive: () => editor.value.isActive('blockquote'),
},
{
type: 'divider',
},
{
type: 'button',
icon: TextWrap,
title: 'Hard break',
action: () => editor.value.chain().focus().setHardBreak().run(),
},
{
type: 'button',
icon: ClearFormatting,
title: 'Clear format',
action: () => editor.value.chain().focus().clearNodes().unsetAllMarks().run(),
},
{
type: 'button',
icon: ArrowBack,
title: 'Undo',
action: () => editor.value.chain().focus().undo().run(),
},
{
type: 'button',
icon: ArrowForwardUp,
title: 'Redo',
action: () => editor.value.chain().focus().redo().run(),
},
];
</script>
<style scoped></style>

View File

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

View File

@@ -0,0 +1,11 @@
import { Edit } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'HTML WYSIWYG editor',
path: '/html-wysiwyg-editor',
description: 'Online HTML editor with feature-rich WYSIWYG editor, get the source code of the content immediately.',
keywords: ['html', 'wysiwyg', 'editor', 'p', 'ul', 'ol', 'converter', 'live'],
component: () => import('./html-wysiwyg-editor.vue'),
icon: Edit,
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,29 @@
import { LockOpen } from '@vicons/tabler';
import type { ToolCategory } from './tool';
import { tool as chmodCalculator } from './chmod-calculator';
import { tool as mimeTypes } from './mime-types';
import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as phoneParserAndFormatter } from './phone-parser-and-formatter';
import { tool as jsonDiff } from './json-diff';
import { tool as ipv4RangeExpander } from './ipv4-range-expander';
import { tool as httpStatusCodes } from './http-status-codes';
import { tool as yamlToJson } from './yaml-to-json-converter';
import { tool as jsonToYaml } from './json-to-yaml-converter';
import { tool as ipv6UlaGenerator } from './ipv6-ula-generator';
import { tool as ipv4AddressConverter } from './ipv4-address-converter';
import { tool as benchmarkBuilder } from './benchmark-builder';
import { tool as userAgentParser } from './user-agent-parser';
import { tool as ipv4SubnetCalculator } from './ipv4-subnet-calculator';
import { tool as dockerRunToDockerComposeConverter } from './docker-run-to-docker-compose-converter';
import { tool as htmlWysiwygEditor } from './html-wysiwyg-editor';
import { tool as svgMeshGradientGenerator } from './svg-mesh-gradient-generator';
import { tool as rsaKeyPairGenerator } from './rsa-key-pair-generator';
import { tool as textToNatoAlphabet } from './text-to-nato-alphabet';
import { tool as slugifyString } from './slugify-string';
import { tool as keycodeInfo } from './keycode-info';
import { tool as jsonMinify } from './json-minify';
import { tool as bcrypt } from './bcrypt';
import { tool as bip39 } from './bip39-generator';
import { tool as caseConverter } from './case-converter';
import { tool as chmodCalculator } from './chmod-calculator';
import { tool as chronometer } from './chronometer';
import { tool as colorConverter } from './color-converter';
import { tool as crontabGenerator } from './crontab-generator';
@@ -23,29 +37,33 @@ import { tool as hmacGenerator } from './hmac-generator';
import { tool as htmlEntities } from './html-entities';
import { tool as baseConverter } from './integer-base-converter';
import { tool as jsonViewer } from './json-viewer';
import { tool as jwtParser } from './jwt-parser';
import { tool as loremIpsumGenerator } from './lorem-ipsum-generator';
import { tool as mathEvaluator } from './math-evaluator';
import { tool as metaTagGenerator } from './meta-tag-generator';
import { tool as mimeTypes } from './mime-types';
import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
import { tool as qrCodeGenerator } from './qr-code-generator';
import { tool as randomPortGenerator } from './random-port-generator';
import { tool as romanNumeralConverter } from './roman-numeral-converter';
import { tool as sqlPrettify } from './sql-prettify';
import { tool as svgPlaceholderGenerator } from './svg-placeholder-generator';
import { tool as temperatureConverter } from './temperature-converter';
import { tool as textStatistics } from './text-statistics';
import { tool as tokenGenerator } from './token-generator';
import type { ToolCategory } from './tools.types';
import { tool as urlEncoder } from './url-encoder';
import { tool as urlParser } from './url-parser';
import { tool as uuidGenerator } from './uuid-generator';
import { tool as macAddressLookup } from './mac-address-lookup';
export const toolsByCategory: ToolCategory[] = [
{
name: 'Crypto',
icon: LockOpen,
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator],
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator],
},
{
name: 'Converter',
icon: LockOpen,
components: [
dateTimeConverter,
baseConverter,
@@ -54,11 +72,13 @@ export const toolsByCategory: ToolCategory[] = [
base64FileConverter,
colorConverter,
caseConverter,
textToNatoAlphabet,
yamlToJson,
jsonToYaml,
],
},
{
name: 'Web',
icon: LockOpen,
components: [
urlEncoder,
htmlEntities,
@@ -68,33 +88,52 @@ export const toolsByCategory: ToolCategory[] = [
metaTagGenerator,
otpCodeGeneratorAndValidator,
mimeTypes,
jwtParser,
keycodeInfo,
slugifyString,
htmlWysiwygEditor,
userAgentParser,
httpStatusCodes,
jsonDiff,
],
},
{
name: 'Images',
icon: LockOpen,
components: [qrCodeGenerator, svgPlaceholderGenerator],
components: [qrCodeGenerator, svgPlaceholderGenerator, svgMeshGradientGenerator],
},
{
name: 'Development',
icon: LockOpen,
components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify, chmodCalculator],
components: [
gitMemo,
randomPortGenerator,
crontabGenerator,
jsonViewer,
jsonMinify,
sqlPrettify,
chmodCalculator,
dockerRunToDockerComposeConverter,
],
},
{
name: 'Network',
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, ipv6UlaGenerator],
},
{
name: 'Math',
icon: LockOpen,
components: [mathEvaluator, etaCalculator],
},
{
name: 'Measurement',
icon: LockOpen,
components: [chronometer],
components: [chronometer, temperatureConverter, benchmarkBuilder],
},
{
name: 'Text',
icon: LockOpen,
components: [loremIpsumGenerator, textStatistics],
},
{
name: 'Data',
components: [phoneParserAndFormatter],
},
];
export const tools = toolsByCategory.flatMap(({ components }) => components);

View File

@@ -1,14 +1,14 @@
<template>
<div>
<n-card>
<c-card>
<div v-if="styleStore.isSmallScreen">
<n-input-group>
<n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
<n-input v-model:value="input" style="width: 100%" :status="error ? 'error' : undefined" />
<n-input v-model:value="input" w-full :status="error ? 'error' : undefined" />
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label>
<n-input-number v-model:value="inputBase" max="64" min="2" style="width: 100%" />
<n-input-number v-model:value="inputBase" max="64" min="2" w-full />
</n-input-group>
</div>
@@ -75,7 +75,7 @@
:placeholder="`Base ${outputBase} will be here...`"
/>
</n-input-group>
</n-card>
</c-card>
</div>
</template>

View File

@@ -0,0 +1,12 @@
import { Binary } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Ipv4 address converter',
path: '/ipv4-address-converter',
description: 'Convert an ip address into decimal, binary, hexadecimal or event in ipv6',
keywords: ['ipv4', 'address', 'converter', 'decimal', 'hexadecimal', 'binary', 'ipv6'],
component: () => import('./ipv4-address-converter.vue'),
icon: Binary,
createdAt: new Date('2023-04-08'),
});

View File

@@ -0,0 +1,36 @@
import { expect, describe, it } from 'vitest';
import { isValidIpv4, ipv4ToInt } from './ipv4-address-converter.service';
describe('ipv4-address-converter', () => {
describe('ipv4ToInt', () => {
it('should convert an IPv4 address to an integer', () => {
expect(ipv4ToInt({ ip: '192.168.0.1' })).toBe(3232235521);
expect(ipv4ToInt({ ip: '10.0.0.1' })).toBe(167772161);
expect(ipv4ToInt({ ip: '255.255.255.255' })).toBe(4294967295);
});
});
describe('isValidIpv4', () => {
it('should return true for a valid IP address', () => {
expect(isValidIpv4({ ip: '192.168.0.1' })).to.equal(true);
expect(isValidIpv4({ ip: '10.0.0.1' })).to.equal(true);
});
it('should return false for an invalid IP address', () => {
expect(isValidIpv4({ ip: '256.168.0.1' })).to.equal(false);
expect(isValidIpv4({ ip: '192.168.0' })).to.equal(false);
expect(isValidIpv4({ ip: '192.168.0.1.2' })).to.equal(false);
expect(isValidIpv4({ ip: '192.168.0.1.' })).to.equal(false);
expect(isValidIpv4({ ip: '.192.168.0.1' })).to.equal(false);
expect(isValidIpv4({ ip: '192.168.0.a' })).to.equal(false);
});
it('should return false for crap as input', () => {
expect(isValidIpv4({ ip: '' })).to.equal(false);
expect(isValidIpv4({ ip: ' ' })).to.equal(false);
expect(isValidIpv4({ ip: 'foo' })).to.equal(false);
expect(isValidIpv4({ ip: '-1' })).to.equal(false);
expect(isValidIpv4({ ip: '0' })).to.equal(false);
});
});
});

View File

@@ -0,0 +1,38 @@
import _ from 'lodash';
export { ipv4ToInt, ipv4ToIpv6, isValidIpv4 };
function ipv4ToInt({ ip }: { ip: string }) {
if (!isValidIpv4({ ip })) {
return 0;
}
return ip
.trim()
.split('.')
.reduce((acc, part, index) => acc + Number(part) * Math.pow(256, 3 - index), 0);
}
function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: string; prefix?: string }) {
if (!isValidIpv4({ ip })) {
return '';
}
return (
prefix +
_.chain(ip)
.trim()
.split('.')
.map((part) => parseInt(part).toString(16).padStart(2, '0'))
.chunk(2)
.map((blocks) => blocks.join(''))
.join(':')
.value()
);
}
function isValidIpv4({ ip }: { ip: string }) {
const cleanIp = ip.trim();
return /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/.test(cleanIp);
}

View File

@@ -0,0 +1,64 @@
<template>
<div>
<n-form-item label="An ipv4 address:" v-bind="validationAttrs">
<n-input v-model:value="rawIpAddress" placeholder="An ipv4 address..." />
</n-form-item>
<n-divider style="margin-top: 0" mt-0 />
<n-form-item
v-for="{ label, value } of convertedSections"
:key="label"
:label="label"
label-placement="left"
label-width="100"
>
<input-copyable
:value="validationAttrs.validationStatus === 'error' ? '' : value"
placeholder="Set a correct ipv4 address"
/>
</n-form-item>
</div>
</template>
<script setup lang="ts">
import { useValidation } from '@/composable/validation';
import { convertBase } from '../integer-base-converter/integer-base-converter.model';
import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from './ipv4-address-converter.service';
const rawIpAddress = useStorage('ipv4-converter:ip', '192.168.1.1');
const convertedSections = computed(() => {
const ipInDecimal = ipv4ToInt({ ip: rawIpAddress.value });
return [
{
label: 'Decimal : ',
value: String(ipInDecimal),
},
{
label: 'Hexadecimal: ',
value: convertBase({ fromBase: 10, toBase: 16, value: String(ipInDecimal) }).toUpperCase(),
},
{
label: 'Binary: ',
value: convertBase({ fromBase: 10, toBase: 2, value: String(ipInDecimal) }),
},
{
label: 'Ipv6: ',
value: ipv4ToIpv6({ ip: rawIpAddress.value }),
},
{
label: 'Ipv6 (short): ',
value: ipv4ToIpv6({ ip: rawIpAddress.value, prefix: '::ffff:' }),
},
];
});
const { attrs: validationAttrs } = useValidation({
source: rawIpAddress,
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }],
});
</script>
<style lang="less" scoped></style>

View File

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

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