mirror of
				https://github.com/CorentinTh/it-tools.git
				synced 2025-10-31 03:53:52 +00:00 
			
		
		
		
	Compare commits
	
		
			173 Commits
		
	
	
		
			v2.8.0
			...
			update-dep
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | f7a1383da5 | ||
|  | c68a1fd713 | ||
|  | 46b1a07213 | ||
|  | dbad7730f9 | ||
|  | 85cb0ffabd | ||
|  | 8355bd2ae4 | ||
|  | 6fb4994603 | ||
|  | 7d7cc99866 | ||
|  | dce9ff91e2 | ||
|  | 80401b6405 | ||
|  | fd9ab59172 | ||
|  | 5fa811a583 | ||
|  | c0a89131dd | ||
|  | 05f06f6a07 | ||
|  | 9fa4c26929 | ||
|  | 9d639edf2d | ||
|  | a1e983538c | ||
|  | 1d7f8b9a8c | ||
|  | ec7cb9351c | ||
|  | ebfdb64fde | ||
|  | d2a2686705 | ||
|  | d7a503b4ae | ||
|  | 28145e0ffe | ||
|  | 8930e139b2 | ||
|  | 076df11024 | ||
|  | 1060652590 | ||
|  | f5c865b278 | ||
|  | 001031b7b5 | ||
|  | 4ccd73c2d1 | ||
|  | 2293f63a79 | ||
|  | 0161395585 | ||
|  | 4d011f15a1 | ||
|  | 4a1afb2b69 | ||
|  | f350dc19aa | ||
|  | f3480fe560 | ||
|  | f68e859c20 | ||
|  | 4872d71165 | ||
|  | fb8a3a0fee | ||
|  | 20282987e3 | ||
|  | ff5f38bec6 | ||
|  | 6e84ea4061 | ||
|  | 004cb83719 | ||
|  | aa4dc0418e | ||
|  | a599528044 | ||
|  | b3390f6ff7 | ||
|  | dee5586bf5 | ||
|  | 5281824b5d | ||
|  | 15cb03347c | ||
|  | 24ba0ff5fa | ||
|  | 849981d1ec | ||
|  | 3b625fd473 | ||
|  | 863c8d0f6a | ||
|  | 9b4f7727f2 | ||
|  | cf16cb195d | ||
|  | f6237376e1 | ||
|  | 165dc93f83 | ||
|  | 161a21f285 | ||
|  | 972ffe6f69 | ||
|  | c339ab3551 | ||
|  | 47948dd343 | ||
|  | 91369cb238 | ||
|  | ffb99579ba | ||
|  | f512d09227 | ||
|  | 7c40539ef9 | ||
|  | f3b1863f09 | ||
|  | b1d6bfd2dc | ||
|  | 8787ce72ab | ||
|  | dab8d63390 | ||
|  | 96aaa35ea2 | ||
|  | 519b169336 | ||
|  | 3bc1f0d1fa | ||
|  | b519cc9574 | ||
|  | 30e9149d61 | ||
|  | 7958e2e075 | ||
|  | 61024279b5 | ||
|  | ad202bd372 | ||
|  | 400654b6b1 | ||
|  | 3351b70c1a | ||
|  | 53ce079dff | ||
|  | a771346250 | ||
|  | a312dedf65 | ||
|  | 0ddf18f4b5 | ||
|  | 9634f5d9a8 | ||
|  | e6c0445684 | ||
|  | 32f87f3fca | ||
|  | a3b1cb5737 | ||
|  | 6fe4b5ac60 | ||
|  | 1a3f0a135d | ||
|  | 072083832d | ||
|  | c934c4e50c | ||
|  | 4a5734d4a3 | ||
|  | f708f5091e | ||
|  | db817a2459 | ||
|  | 119041c185 | ||
|  | 4607837f9a | ||
|  | f52f7a845c | ||
|  | acc7f0a586 | ||
|  | ebb7301a98 | ||
|  | def60e7248 | ||
|  | bf88836dbe | ||
|  | bfc2e24bbf | ||
|  | 40872859a5 | ||
|  | cf723f144e | ||
|  | 7f964941d3 | ||
|  | af075dcccc | ||
|  | 274ff02b54 | ||
|  | 679dd1c1f6 | ||
|  | 4cd809bd0c | ||
|  | 8d09086e78 | ||
|  | acf8bc11db | ||
|  | 71e98e93e5 | ||
|  | 1b5d4e72bd | ||
|  | 8476cf319b | ||
|  | 0ff853437b | ||
|  | 39c8f92065 | ||
|  | 35b5187119 | ||
|  | 94698cea50 | ||
|  | 8294cd68da | ||
|  | 7c9b8ac178 | ||
|  | 5d8f46abf8 | ||
|  | 35a3760771 | ||
|  | 4ef25887b9 | ||
|  | 7f229959d6 | ||
|  | d3a2936979 | ||
|  | 5f16885923 | ||
|  | ea5e7a7fc7 | ||
|  | 7de6c86f9e | ||
|  | 83da6b7ee9 | ||
|  | 737319edf1 | ||
|  | a77a82f5a2 | ||
|  | da17696293 | ||
|  | 164e32b442 | ||
|  | 49755909bd | ||
|  | 44d653b1f2 | ||
|  | 7c449f4f2d | ||
|  | ab7483b5c2 | ||
|  | 5222bd5d04 | ||
|  | cf5e4d9056 | ||
|  | 992f96b48a | ||
|  | fcf4cfe64d | ||
|  | f54223fb0a | ||
|  | b38ab82d05 | ||
|  | f6cd9b76d3 | ||
|  | 208a373fd0 | ||
|  | 8089c60000 | ||
|  | d30cd8a9ab | ||
|  | 04a8e122be | ||
|  | 447bdf2148 | ||
|  | ca7cb44389 | ||
|  | e48d60b1ed | ||
|  | fda0b0ca25 | ||
|  | cc717bc87e | ||
|  | 1bc6380c6f | ||
|  | 02c4963531 | ||
|  | 129f74c371 | ||
|  | 0be33fb337 | ||
|  | 422b6eb05a | ||
|  | fad4833ca2 | ||
|  | 531a25c1c4 | ||
|  | 77b5b0cab5 | ||
|  | 7570ad9656 | ||
|  | 8a9e7888de | ||
|  | 750a76b00f | ||
|  | 5f03619ab4 | ||
|  | 352365f012 | ||
|  | 4f599b6999 | ||
|  | 138149e6f0 | ||
|  | 412de23796 | ||
|  | 1a22d55b3c | ||
|  | bb4aac6d4a | ||
|  | e6953d1b67 | ||
|  | a70a0f83a1 | ||
|  | bdee93a9e4 | 
							
								
								
									
										264
									
								
								.eslintrc-auto-import.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								.eslintrc-auto-import.json
									
									
									
									
									
										Normal 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 | ||||
|   } | ||||
| } | ||||
| @@ -7,24 +7,33 @@ module.exports = { | ||||
|     'plugin:vue/vue3-essential', | ||||
|     'eslint:recommended', | ||||
|     'plugin:vue/vue3-recommended', | ||||
|     'plugin:vue/vue3-recommended', | ||||
|     '@vue/eslint-config-typescript/recommended', | ||||
|     '@vue/eslint-config-prettier', | ||||
|     'plugin:import/recommended', | ||||
|     './.eslintrc-auto-import.json', | ||||
|     '@unocss', | ||||
|   ], | ||||
|  | ||||
|   settings: { | ||||
|     'import/resolver': { typescript: { project: './tsconfig.app.json' } }, | ||||
|   }, | ||||
|   env: { | ||||
|     'vue/setup-compiler-macros': true, | ||||
|   }, | ||||
|   rules: { | ||||
|     'vue/multi-word-component-names': ['off'], | ||||
|     'prettier/prettier': [ | ||||
|     'prettier/prettier': ['error'], | ||||
|     'import/no-duplicates': ['error', { considerQueryString: true }], | ||||
|     'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }], | ||||
|     'import/extensions': [ | ||||
|       'error', | ||||
|       'ignorePackages', | ||||
|       { | ||||
|         singleQuote: true, | ||||
|         semi: true, | ||||
|         tabWidth: 2, | ||||
|         trailingComma: 'all', | ||||
|         printWidth: 120, | ||||
|         js: 'never', | ||||
|         ts: 'never', | ||||
|         tsx: 'never', | ||||
|       }, | ||||
|     ], | ||||
|     'import/no-unresolved': ['error', { ignore: ['^virtual:'] }], | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -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. | ||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							| @@ -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. | ||||
							
								
								
									
										19
									
								
								.github/ISSUE_TEMPLATE/new-tool-request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.github/ISSUE_TEMPLATE/new-tool-request.md
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										13
									
								
								.github/ISSUE_TEMPLATE/other-request.md
									
									
									
									
										vendored
									
									
										Normal 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. | ||||
							
								
								
									
										13
									
								
								.github/ISSUE_TEMPLATE/tool-improvement.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.github/ISSUE_TEMPLATE/tool-improvement.md
									
									
									
									
										vendored
									
									
										Normal 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. | ||||
							
								
								
									
										33
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,38 +1,31 @@ | ||||
| name: ci | ||||
|  | ||||
| on: [push, pull_request] | ||||
| on: | ||||
|   pull_request: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|  | ||||
| jobs: | ||||
|   ci: | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@master | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|  | ||||
|       - name: Setup node env | ||||
|         uses: actions/setup-node@v3.0.0 | ||||
|       - uses: actions/checkout@v3 | ||||
|       - run: corepack enable | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: 16 | ||||
|  | ||||
|       - name: Cache node_modules | ||||
|         uses: actions/cache@v2 | ||||
|         with: | ||||
|           path: ~/.npm | ||||
|           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} | ||||
|           restore-keys: | | ||||
|             ${{ runner.os }}-node- | ||||
|           cache: 'pnpm' | ||||
|  | ||||
|       - name: Install dependencies | ||||
|         run: npm ci | ||||
|         run: pnpm i | ||||
|  | ||||
|       - name: Run linters | ||||
|         run: npm run lint | ||||
|         run: pnpm lint | ||||
|  | ||||
|       - name: Run unit test | ||||
|         run: npm run test | ||||
|         run: pnpm test | ||||
|  | ||||
|       - name: Build the app | ||||
|         run: npm run build | ||||
|         run: pnpm build | ||||
|   | ||||
							
								
								
									
										87
									
								
								.github/workflows/docker-nightly-release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								.github/workflows/docker-nightly-release.yml
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										104
									
								
								.github/workflows/releases.yml
									
									
									
									
										vendored
									
									
										Normal 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
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -26,3 +26,8 @@ coverage | ||||
| *.njsproj | ||||
| *.sln | ||||
| *.sw? | ||||
|  | ||||
| .env | ||||
| /test-results/ | ||||
| /playwright-report/ | ||||
| /playwright/.cache/ | ||||
|   | ||||
							
								
								
									
										7
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "singleQuote": true, | ||||
|   "semi": true, | ||||
|   "tabWidth": 2, | ||||
|   "trailingComma": "all", | ||||
|   "printWidth": 120 | ||||
| } | ||||
							
								
								
									
										353
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										353
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -2,347 +2,54 @@ | ||||
|  | ||||
| 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.8.0](https://github.com/CorentinTh/it-tools/compare/v2.7.0...v2.8.0) (2022-07-24) | ||||
|  | ||||
| ## Version 2023.04.14-dbad773 | ||||
|  | ||||
| ### Features | ||||
| - **new-tool**: http status codes (8355bd2) | ||||
|  | ||||
| * **new-tool:** added an ETA calculator ([125a502](https://github.com/CorentinTh/it-tools/commit/125a50215a7abb9e0b59dbbc62aee49007b05ffe)) | ||||
| ### Refactoring | ||||
| - **uuid-generator**: prevent NaN in quantity (6fb4994) | ||||
|  | ||||
| ### Chores | ||||
| - **release**: create a github release on new version (dbad773) | ||||
| - **version**: reset CHANGELOG content to support new format (85cb0ff) | ||||
|  | ||||
| ### 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) | ||||
|  | ||||
| ## Version 2023.04.14-f9b77b7 | ||||
|  | ||||
| ### Features | ||||
| - **new-tool**: http status codes (8355bd2) | ||||
|  | ||||
| * **new-tool:** added an SQL prettifier and formatter ([d1f95f5](https://github.com/CorentinTh/it-tools/commit/d1f95f5b34a4570f1033a5289f0bd009d1aefb0c)) | ||||
| ### 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 | ||||
|  | ||||
| * **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) | ||||
|  | ||||
| ## Version 2023.04.14-2f0d239 | ||||
|  | ||||
| ### Features | ||||
| - **new-tool**: http status codes (8355bd2) | ||||
|  | ||||
| * **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)) | ||||
| ### 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:** 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) | ||||
|  | ||||
| ## Version 2023.04.14-474cae4 | ||||
|  | ||||
| ### Features | ||||
| - **new-tool**: http status codes (8355bd2) | ||||
|  | ||||
| * **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)) | ||||
| ### 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 | ||||
|  | ||||
| * **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
									
								
							
							
						
						
									
										14
									
								
								Dockerfile
									
									
									
									
									
										Normal 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;"] | ||||
							
								
								
									
										59
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								README.md
									
									
									
									
									
								
							| @@ -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-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT Tools - Collection of handy online tools for devs, with great 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-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 Tools - Collection of handy online tools for devs, with great 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
									
								
							
							
						
						
									
										524
									
								
								auto-imports.d.ts
									
									
									
									
										vendored
									
									
										Normal 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']> | ||||
|   } | ||||
| } | ||||
							
								
								
									
										73
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| /* 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 { | ||||
|     CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default'] | ||||
|     ColoredCard: typeof import('./src/components/ColoredCard.vue')['default'] | ||||
|     FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default'] | ||||
|     FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default'] | ||||
|     InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] | ||||
|     MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default'] | ||||
|     MenuLayout: typeof import('./src/components/MenuLayout.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'] | ||||
|     NH4: typeof import('naive-ui')['NH4'] | ||||
|     NIcon: typeof import('naive-ui')['NIcon'] | ||||
|     NImage: typeof import('naive-ui')['NImage'] | ||||
|     NInput: typeof import('naive-ui')['NInput'] | ||||
|     NInputGroup: typeof import('naive-ui')['NInputGroup'] | ||||
|     NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] | ||||
|     NInputNumber: typeof import('naive-ui')['NInputNumber'] | ||||
|     NLayout: typeof import('naive-ui')['NLayout'] | ||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||
|     NMenu: typeof import('naive-ui')['NMenu'] | ||||
|     NP: typeof import('naive-ui')['NP'] | ||||
|     NPageHeader: typeof import('naive-ui')['NPageHeader'] | ||||
|     NProgress: typeof import('naive-ui')['NProgress'] | ||||
|     NResult: typeof import('naive-ui')['NResult'] | ||||
|     NScrollbar: typeof import('naive-ui')['NScrollbar'] | ||||
|     NSelect: typeof import('naive-ui')['NSelect'] | ||||
|     NSlider: typeof import('naive-ui')['NSlider'] | ||||
|     NSpace: typeof import('naive-ui')['NSpace'] | ||||
|     NStatistic: typeof import('naive-ui')['NStatistic'] | ||||
|     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||
|     NTable: typeof import('naive-ui')['NTable'] | ||||
|     NTag: typeof import('naive-ui')['NTag'] | ||||
|     NText: typeof import('naive-ui')['NText'] | ||||
|     NTooltip: typeof import('naive-ui')['NTooltip'] | ||||
|     NUpload: typeof import('naive-ui')['NUpload'] | ||||
|     NUploadDragger: typeof import('naive-ui')['NUploadDragger'] | ||||
|     RouterLink: typeof import('vue-router')['RouterLink'] | ||||
|     RouterView: typeof import('vue-router')['RouterView'] | ||||
|     SearchBar: typeof import('./src/components/SearchBar.vue')['default'] | ||||
|     SearchBarItem: typeof import('./src/components/SearchBarItem.vue')['default'] | ||||
|     TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default'] | ||||
|     ToolCard: typeof import('./src/components/ToolCard.vue')['default'] | ||||
|   } | ||||
| } | ||||
							
								
								
									
										10
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								index.html
									
									
									
									
									
								
							| @@ -31,17 +31,19 @@ | ||||
|       property="og:description" | ||||
|       content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." | ||||
|     /> | ||||
|     <meta property="og:image" content="/banner.png" /> | ||||
|     <meta property="og:image" content="https://it-tools.tech/banner.png?v=2" /> | ||||
|  | ||||
|     <meta name="twitter:card" content="summary_large_image" /> | ||||
|     <meta property="twitter:domain" content="it-tools.tech" /> | ||||
|     <meta property="twitter:url" content="https://it-tools.tech/" /> | ||||
|     <meta name="twitter:site" content="@ittoolsdottech" /> | ||||
|     <meta name="twitter:creator" content="@cthmsst" /> | ||||
|  | ||||
|     <meta name="twitter:title" content="IT Tools - Handy online tools for developers" /> | ||||
|     <meta | ||||
|       name="twitter:description" | ||||
|       content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." | ||||
|     /> | ||||
|     <meta name="twitter:image" content="/banner.png" /> | ||||
|     <meta name="twitter:image" content="https://it-tools.tech/banner.png?v=2" /> | ||||
|     <meta name="twitter:image:alt" content="IT Tools - Handy online tools for developers" /> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"></div> | ||||
|   | ||||
							
								
								
									
										10
									
								
								nginx.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								nginx.conf
									
									
									
									
									
										Normal 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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19635
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										19635
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										109
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										109
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,8 @@ | ||||
| { | ||||
|   "name": "it-tools", | ||||
|   "version": "2.8.0", | ||||
|   "version": "2023.4.14-dbad773", | ||||
|   "description": "Collection of handy online tools for developers, with great UX. ", | ||||
|   "type": "module", | ||||
|   "keywords": [ | ||||
|     "productivity", | ||||
|     "converter", | ||||
| @@ -25,67 +26,103 @@ | ||||
|     "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.2.1", | ||||
|     "@vueuse/head": "^0.7.5", | ||||
|     "@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.2", | ||||
|     "colord": "^2.9.3", | ||||
|     "composerize-ts": "^0.6.2", | ||||
|     "cron-validator": "^1.3.1", | ||||
|     "cronstrue": "^2.2.0", | ||||
|     "cronstrue": "^2.26.0", | ||||
|     "crypto-js": "^4.1.1", | ||||
|     "date-fns": "^2.28.0", | ||||
|     "date-fns": "^2.29.3", | ||||
|     "figue": "^1.2.0", | ||||
|     "highlight.js": "^11.5.1", | ||||
|     "fuse.js": "^6.6.2", | ||||
|     "highlight.js": "^11.7.0", | ||||
|     "json5": "^2.2.3", | ||||
|     "jwt-decode": "^3.1.2", | ||||
|     "lodash": "^4.17.21", | ||||
|     "mathjs": "^10.6.0", | ||||
|     "naive-ui": "^2.31.0", | ||||
|     "pinia": "^2.0.11", | ||||
|     "plausible-tracker": "^0.3.5", | ||||
|     "qrcode": "^1.5.0", | ||||
|     "mathjs": "^10.6.4", | ||||
|     "mime-types": "^2.1.35", | ||||
|     "naive-ui": "^2.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.31", | ||||
|     "vue-router": "^4.0.12" | ||||
|     "vue": "^3.2.47", | ||||
|     "vue-router": "^4.1.6", | ||||
|     "yaml": "^2.2.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@rushstack/eslint-patch": "^1.1.0", | ||||
|     "@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.14", | ||||
|     "@types/node": "^16.11.25", | ||||
|     "@types/qrcode": "^1.4.2", | ||||
|     "@types/jsdom": "^16.2.15", | ||||
|     "@types/lodash": "^4.14.194", | ||||
|     "@types/mime-types": "^2.1.1", | ||||
|     "@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", | ||||
|     "@vitejs/plugin-vue": "^2.2.2", | ||||
|     "@vitejs/plugin-vue-jsx": "^1.3.7", | ||||
|     "@vue/eslint-config-prettier": "^7.0.0", | ||||
|     "@typescript-eslint/parser": "^5.58.0", | ||||
|     "@unocss/eslint-config": "^0.50.8", | ||||
|     "@vitejs/plugin-vue": "^4.1.0", | ||||
|     "@vitejs/plugin-vue-jsx": "^1.3.10", | ||||
|     "@vue/eslint-config-prettier": "^7.1.0", | ||||
|     "@vue/eslint-config-typescript": "^10.0.0", | ||||
|     "@vue/test-utils": "^2.0.0-rc.18", | ||||
|     "@vue/test-utils": "^2.3.2", | ||||
|     "@vue/tsconfig": "^0.1.3", | ||||
|     "c8": "^7.11.0", | ||||
|     "eslint": "^8.5.0", | ||||
|     "eslint-plugin-vue": "^8.2.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.2", | ||||
|     "prettier": "^2.6.2", | ||||
|     "standard-version": "^9.3.2", | ||||
|     "start-server-and-test": "^1.14.0", | ||||
|     "less": "^4.1.3", | ||||
|     "prettier": "^2.8.7", | ||||
|     "start-server-and-test": "^1.15.4", | ||||
|     "typescript": "~4.5.5", | ||||
|     "vite": "^2.9.1", | ||||
|     "vite-plugin-md": "^0.12.4", | ||||
|     "vite-plugin-pwa": "^0.11.13", | ||||
|     "vite-svg-loader": "^3.2.0", | ||||
|     "vitest": "^0.13.1", | ||||
|     "vue-tsc": "^0.31.4" | ||||
|     "unocss": "^0.50.8", | ||||
|     "unplugin-auto-import": "^0.15.3", | ||||
|     "unplugin-vue-components": "^0.24.1", | ||||
|     "vite": "^4.2.1", | ||||
|     "vite-plugin-md": "^0.21.5", | ||||
|     "vite-plugin-pwa": "^0.14.7", | ||||
|     "vite-svg-loader": "^3.6.0", | ||||
|     "vitest": "^0.30.1", | ||||
|     "vue-tsc": "^1.2.0", | ||||
|     "workbox-window": "^6.5.4", | ||||
|     "zx": "^7.2.1" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										82
									
								
								playwright.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								playwright.config.ts
									
									
									
									
									
										Normal 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, | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										8624
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8624
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 155 KiB | 
| @@ -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) { | ||||
| @@ -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')); | ||||
|  | ||||
|   | ||||
							
								
								
									
										6
									
								
								scripts/getLatestChangelog.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								scripts/getLatestChangelog.mjs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										57
									
								
								scripts/release.mjs
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
							
								
								
									
										15
									
								
								scripts/shared/changelog.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								scripts/shared/changelog.mjs
									
									
									
									
									
										Normal 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'); | ||||
| } | ||||
							
								
								
									
										54
									
								
								scripts/shared/commits.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								scripts/shared/commits.mjs
									
									
									
									
									
										Normal 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(); | ||||
| } | ||||
							
								
								
									
										12
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/App.vue
									
									
									
									
									
								
							| @@ -1,9 +1,9 @@ | ||||
| <script setup lang="ts"> | ||||
| import { layouts } from './layouts'; | ||||
| import { computed } from 'vue'; | ||||
| import { useRoute, RouterView } from 'vue-router'; | ||||
| import { darkTheme, NGlobalStyle, NMessageProvider, NNotificationProvider } from 'naive-ui'; | ||||
| import { darkThemeOverrides, lightThemeOverrides } from './themes'; | ||||
| import { darkTheme, NGlobalStyle, NMessageProvider } from 'naive-ui'; | ||||
| import { layouts } from './layouts'; | ||||
| import { useStyleStore } from './stores/style.store'; | ||||
|  | ||||
| const route = useRoute(); | ||||
| @@ -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> | ||||
|   | ||||
							
								
								
									
										134
									
								
								src/components/CollapsibleToolMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/components/CollapsibleToolMenu.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										59
									
								
								src/components/ColoredCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/components/ColoredCard.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| <template> | ||||
|   <n-card class="colored-card"> | ||||
|     <n-space justify="space-between" align="center"> | ||||
|       <n-icon class="icon" size="40" :component="icon" /> | ||||
|     </n-space> | ||||
|     <n-h3 class="title"> | ||||
|       <n-ellipsis>{{ title }}</n-ellipsis> | ||||
|     </n-h3> | ||||
|  | ||||
|     <div class="description"> | ||||
|       <n-ellipsis :line-clamp="2" :tooltip="false"> | ||||
|         <slot /> | ||||
|       </n-ellipsis> | ||||
|     </div> | ||||
|   </n-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { toRefs, type Component } from 'vue'; | ||||
|  | ||||
| const props = defineProps<{ icon: Component; title: string }>(); | ||||
| const { icon, title } = toRefs(props); | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .colored-card { | ||||
|   background: rgb(37, 99, 108); | ||||
|   background: linear-gradient(48deg, rgba(37, 99, 108, 1) 0%, rgba(59, 149, 111, 1) 60%, rgba(20, 160, 88, 1) 100%); | ||||
|   color: #fff; | ||||
|   border: none; | ||||
|  | ||||
|   .icon { | ||||
|     opacity: 0.7; | ||||
|   } | ||||
|  | ||||
|   .title { | ||||
|     color: #fff; | ||||
|  | ||||
|     margin: 5px 0; | ||||
|   } | ||||
|  | ||||
|   .description { | ||||
|     opacity: 0.8; | ||||
|  | ||||
|     margin: 5px 0; | ||||
|  | ||||
|     ::v-deep(a) { | ||||
|       color: inherit; | ||||
|       text-decoration: underline; | ||||
|       font-weight: bold; | ||||
|       transition: color ease 0.2s; | ||||
|  | ||||
|       &:hover { | ||||
|         color: rgb(20, 20, 20); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										38
									
								
								src/components/FavoriteButton.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/components/FavoriteButton.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| <template> | ||||
|   <n-tooltip trigger="hover"> | ||||
|     <template #trigger> | ||||
|       <n-button circle quaternary :type="buttonType" :style="{ opacity: isFavorite ? 1 : 0.2 }" @click="toggleFavorite"> | ||||
|         <template #icon> | ||||
|           <n-icon :component="FavoriteFilled" /> | ||||
|         </template> | ||||
|       </n-button> | ||||
|     </template> | ||||
|     {{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }} | ||||
|   </n-tooltip> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { FavoriteFilled } from '@vicons/material'; | ||||
| import { useToolStore } from '@/tools/tools.store'; | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
| import { computed, toRefs } from 'vue'; | ||||
|  | ||||
| const 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> | ||||
							
								
								
									
										57
									
								
								src/components/FormatTransformer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/components/FormatTransformer.vue
									
									
									
									
									
										Normal 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> | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -9,7 +9,7 @@ | ||||
|         href="https://github.com/CorentinTh/it-tools" | ||||
|         rel="noopener" | ||||
|         target="_blank" | ||||
|         aria-label="IT-Tools' github repository" | ||||
|         aria-label="IT-Tools' GitHub repository" | ||||
|       > | ||||
|         <n-icon size="25" :component="BrandGithub" /> | ||||
|       </n-button> | ||||
| @@ -27,12 +27,12 @@ | ||||
|         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> | ||||
|     </template> | ||||
|     IT Tools' twitter account | ||||
|     IT Tools' Twitter account | ||||
|   </n-tooltip> | ||||
|  | ||||
|   <router-link to="/about" #="{ navigate, href }" custom> | ||||
|   | ||||
| @@ -1,36 +1,38 @@ | ||||
| <script lang="ts" setup> | ||||
| import { useFuzzySearch } from '@/composable/fuzzySearch'; | ||||
| import { useTracker } from '@/modules/tracker/tracker.services'; | ||||
| import { tools } from '@/tools'; | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
| import { SearchRound } from '@vicons/material'; | ||||
| import { useMagicKeys, whenever } from '@vueuse/core'; | ||||
| import { deburr } from 'lodash'; | ||||
| import { computed, ref } from 'vue'; | ||||
| 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 cleanString = (s: string) => deburr(s.trim().toLowerCase()); | ||||
|  | ||||
| const searchableTools = tools.map(({ name, description, keywords, path }) => ({ | ||||
|   searchableText: [name, description, ...keywords].map(cleanString).join(' '), | ||||
|   path, | ||||
|   name, | ||||
| })); | ||||
| const inputEl = ref<HTMLElement>(); | ||||
| const displayDropDown = ref(true); | ||||
| const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac')); | ||||
|  | ||||
| const options = computed(() => { | ||||
|   const query = cleanString(queryString.value); | ||||
|   if (queryString.value === '') { | ||||
|     return tools.map(toolToOption); | ||||
|   } | ||||
|  | ||||
|   return searchableTools | ||||
|     .filter(({ searchableText }) => searchableText.includes(query)) | ||||
|     .map(({ name, path }) => ({ label: name, value: path })); | ||||
|   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, | ||||
| @@ -38,12 +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: 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> | ||||
| @@ -51,16 +81,21 @@ whenever(keys.ctrl_k, () => { | ||||
|     <n-auto-complete | ||||
|       v-model:value="queryString" | ||||
|       :options="options" | ||||
|       :input-props="{ autocomplete: 'disabled' }" | ||||
|       :on-select="onSelect" | ||||
|       :on-select="(value) => onSelect(String(value))" | ||||
|       :render-label="renderOption" | ||||
|       :default-value="'aa'" | ||||
|       :get-show="() => displayDropDown" | ||||
|       :on-focus="onFocus" | ||||
|       @update:value="() => (displayDropDown = true)" | ||||
|     > | ||||
|       <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }"> | ||||
|         <n-input | ||||
|           ref="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" | ||||
|           @focus="handleFocus" | ||||
|           @blur="handleBlur" | ||||
| @@ -73,9 +108,3 @@ whenever(keys.ctrl_k, () => { | ||||
|     </n-auto-complete> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| // ::v-deep(.n-input__border) { | ||||
| //     border: none; | ||||
| // } | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										45
									
								
								src/components/SearchBarItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/components/SearchBarItem.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| <script lang="ts" setup> | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
| import { toRefs } from 'vue'; | ||||
|  | ||||
| const props = defineProps<{ tool: Tool }>(); | ||||
| const { tool } = toRefs(props); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div class="search-bar-item"> | ||||
|     <n-icon class="icon" :component="tool.icon" /> | ||||
|  | ||||
|     <div> | ||||
|       <div class="name">{{ tool.name }}</div> | ||||
|       <div class="description">{{ tool.description }}</div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .search-bar-item { | ||||
|   padding: 10px; | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   align-items: center; | ||||
|  | ||||
|   .icon { | ||||
|     font-size: 30px; | ||||
|     margin-right: 10px; | ||||
|     opacity: 0.7; | ||||
|   } | ||||
|  | ||||
|   .name { | ||||
|     font-weight: bold; | ||||
|     font-size: 15px; | ||||
|     line-height: 1; | ||||
|     margin-bottom: 5px; | ||||
|   } | ||||
|  | ||||
|   .description { | ||||
|     opacity: 0.7; | ||||
|     line-height: 1; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										102
									
								
								src/components/TextareaCopyable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/components/TextareaCopyable.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| <template> | ||||
|   <div style="overflow-x: hidden; width: 100%"> | ||||
|     <n-card class="result-card"> | ||||
|       <n-scrollbar | ||||
|         x-scrollable | ||||
|         trigger="none" | ||||
|         :style="height ? `min-height: ${height - 40 /* card padding */ + 10 /* negative margin compensation */}px` : ''" | ||||
|       > | ||||
|         <n-config-provider :hljs="hljs"> | ||||
|           <n-code :code="value" :language="language" :trim="false" data-test-id="area-content" /> | ||||
|         </n-config-provider> | ||||
|       </n-scrollbar> | ||||
|       <n-tooltip v-if="value" trigger="hover"> | ||||
|         <template #trigger> | ||||
|           <div class="copy-button" :class="[copyPlacement]"> | ||||
|             <n-button circle secondary size="large" @click="onCopyClicked"> | ||||
|               <n-icon size="22" :component="Copy" /> | ||||
|             </n-button> | ||||
|           </div> | ||||
|         </template> | ||||
|         <span>{{ tooltipText }}</span> | ||||
|       </n-tooltip> | ||||
|     </n-card> | ||||
|     <n-space v-if="copyPlacement === 'outside'" justify="center" mt-4> | ||||
|       <n-button secondary @click="onCopyClicked"> {{ tooltipText }} </n-button> | ||||
|     </n-space> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { Copy } from '@vicons/tabler'; | ||||
| import { useClipboard, useElementSize } from '@vueuse/core'; | ||||
| import hljs from 'highlight.js/lib/core'; | ||||
| import jsonHljs from 'highlight.js/lib/languages/json'; | ||||
| import sqlHljs from 'highlight.js/lib/languages/sql'; | ||||
| import xmlHljs from 'highlight.js/lib/languages/xml'; | ||||
| import yamlHljs from 'highlight.js/lib/languages/yaml'; | ||||
| import { ref, toRefs } from 'vue'; | ||||
|  | ||||
| hljs.registerLanguage('sql', sqlHljs); | ||||
| hljs.registerLanguage('json', jsonHljs); | ||||
| hljs.registerLanguage('html', xmlHljs); | ||||
| hljs.registerLanguage('yaml', yamlHljs); | ||||
|  | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     value: string; | ||||
|     followHeightOf?: HTMLElement | null; | ||||
|     language?: string; | ||||
|     copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none'; | ||||
|     copyMessage?: string; | ||||
|   }>(), | ||||
|   { | ||||
|     followHeightOf: null, | ||||
|     language: 'txt', | ||||
|     copyPlacement: 'top-right', | ||||
|     copyMessage: 'Copy to clipboard', | ||||
|   }, | ||||
| ); | ||||
| const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); | ||||
| const { height } = followHeightOf ? useElementSize(followHeightOf) : { height: ref(null) }; | ||||
|  | ||||
| const { copy } = useClipboard({ source: value }); | ||||
| const tooltipText = ref(copyMessage.value); | ||||
|  | ||||
| function onCopyClicked() { | ||||
|   copy(); | ||||
|   tooltipText.value = 'Copied !'; | ||||
|  | ||||
|   setTimeout(() => { | ||||
|     tooltipText.value = copyMessage.value; | ||||
|   }, 2000); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| ::v-deep(.n-scrollbar) { | ||||
|   padding-bottom: 10px; | ||||
|   margin-bottom: -10px; | ||||
| } | ||||
| .result-card { | ||||
|   position: relative; | ||||
|   .copy-button { | ||||
|     position: absolute; | ||||
|     opacity: 1; | ||||
|  | ||||
|     &.top-right { | ||||
|       top: 10px; | ||||
|       right: 10px; | ||||
|     } | ||||
|  | ||||
|     &.bottom-right { | ||||
|       bottom: 10px; | ||||
|       right: 10px; | ||||
|     } | ||||
|     &.outside, | ||||
|     &.none { | ||||
|       display: none; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @@ -3,25 +3,30 @@ | ||||
|     <n-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 />  | ||||
|         </n-ellipsis> | ||||
|       </div> | ||||
|     </n-card> | ||||
| @@ -29,11 +34,12 @@ | ||||
| </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 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(); | ||||
| </script> | ||||
| @@ -49,7 +55,8 @@ a { | ||||
|   } | ||||
|  | ||||
|   .icon { | ||||
|     opacity: 0.7; | ||||
|     opacity: 0.6; | ||||
|     color: v-bind('theme.textColorBase'); | ||||
|   } | ||||
|  | ||||
|   .title { | ||||
| @@ -57,7 +64,8 @@ a { | ||||
|   } | ||||
|  | ||||
|   .description { | ||||
|     opacity: 0.7; | ||||
|     opacity: 0.6; | ||||
|     color: v-bind('theme.textColorBase'); | ||||
|     margin: 5px 0; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										46
									
								
								src/composable/computedRefreshable.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/composable/computedRefreshable.ts
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
| @@ -1,11 +1,36 @@ | ||||
| import { extension as getExtensionFromMime } from 'mime-types'; | ||||
| import type { Ref } from 'vue'; | ||||
|  | ||||
| export function useDownloadFileFromBase64({ source, filename = 'file' }: { source: Ref<string>; filename?: string }) { | ||||
| function getFileExtensionFromBase64({ | ||||
|   base64String, | ||||
|   defaultExtension = 'txt', | ||||
| }: { | ||||
|   base64String: string; | ||||
|   defaultExtension?: string; | ||||
| }) { | ||||
|   const hasMimeType = base64String.match(/data:(.*?);base64/i); | ||||
|  | ||||
|   if (hasMimeType) { | ||||
|     return getExtensionFromMime(hasMimeType[1]) || defaultExtension; | ||||
|   } | ||||
|  | ||||
|   return defaultExtension; | ||||
| } | ||||
|  | ||||
| export function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) { | ||||
|   return { | ||||
|     download() { | ||||
|       const base64String = source.value; | ||||
|  | ||||
|       if (base64String === '') { | ||||
|         throw new Error('Base64 string is empty'); | ||||
|       } | ||||
|  | ||||
|       const cleanFileName = filename ?? `file.${getFileExtensionFromBase64({ base64String })}`; | ||||
|  | ||||
|       const a = document.createElement('a'); | ||||
|       a.href = source.value; | ||||
|       a.download = filename; | ||||
|       a.href = base64String; | ||||
|       a.download = cleanFileName; | ||||
|       a.click(); | ||||
|     }, | ||||
|   }; | ||||
|   | ||||
							
								
								
									
										23
									
								
								src/composable/fuzzySearch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/composable/fuzzySearch.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { get, type MaybeRef } from '@vueuse/core'; | ||||
| import Fuse from 'fuse.js'; | ||||
| import { computed } from 'vue'; | ||||
|  | ||||
| export { useFuzzySearch }; | ||||
|  | ||||
| function useFuzzySearch<Data>({ | ||||
|   search, | ||||
|   data, | ||||
|   options = {}, | ||||
| }: { | ||||
|   search: MaybeRef<string>; | ||||
|   data: Data[]; | ||||
|   options?: Fuse.IFuseOptions<Data>; | ||||
| }) { | ||||
|   const fuse = new Fuse(data, options); | ||||
|  | ||||
|   const searchResult = computed(() => { | ||||
|     return fuse.search(get(search)).map(({ item }) => item); | ||||
|   }); | ||||
|  | ||||
|   return { searchResult }; | ||||
| } | ||||
							
								
								
									
										35
									
								
								src/composable/queryParams.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/composable/queryParams.ts
									
									
									
									
									
										Normal 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 T; | ||||
|     }, | ||||
|     set(value) { | ||||
|       proxy.value = transformer.toQuery(value as never); | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										29
									
								
								src/composable/validation.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/composable/validation.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| /* eslint-disable @typescript-eslint/no-empty-function */ | ||||
| import { describe, expect, it } from 'vitest'; | ||||
| import { isFalsyOrHasThrown } from './validation'; | ||||
|  | ||||
| describe('useValidation', () => { | ||||
|   describe('isFalsyOrHasThrown', () => { | ||||
|     it('should return true if the callback return nil, false or throw', () => { | ||||
|       expect(isFalsyOrHasThrown(() => false)).toBe(true); | ||||
|       expect(isFalsyOrHasThrown(() => null)).toBe(true); | ||||
|       expect(isFalsyOrHasThrown(() => undefined)).toBe(true); | ||||
|       expect(isFalsyOrHasThrown(() => {})).toBe(true); | ||||
|       expect( | ||||
|         isFalsyOrHasThrown(() => { | ||||
|           throw new Error(); | ||||
|         }), | ||||
|       ).toBe(true); | ||||
|     }); | ||||
|  | ||||
|     it('should return true for any truthy values and empty string and 0 values', () => { | ||||
|       expect(isFalsyOrHasThrown(() => true)).toBe(false); | ||||
|       expect(isFalsyOrHasThrown(() => 'string')).toBe(false); | ||||
|       expect(isFalsyOrHasThrown(() => 1)).toBe(false); | ||||
|       expect(isFalsyOrHasThrown(() => 0)).toBe(false); | ||||
|       expect(isFalsyOrHasThrown(() => '')).toBe(false); | ||||
|       expect(isFalsyOrHasThrown(() => [])).toBe(false); | ||||
|       expect(isFalsyOrHasThrown(() => ({}))).toBe(false); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,38 +1,73 @@ | ||||
| import _ from 'lodash'; | ||||
| import { reactive, watch, type Ref } from 'vue'; | ||||
|  | ||||
| type UseValidationRule<T> = { | ||||
|   validator: (value: T) => boolean; | ||||
|   message: string; | ||||
| }; | ||||
| type ValidatorReturnType = unknown; | ||||
|  | ||||
| function isFalsyOrHasThrown(cb: () => boolean) { | ||||
| export interface UseValidationRule<T> { | ||||
|   validator: (value: T) => ValidatorReturnType; | ||||
|   message: string; | ||||
| } | ||||
|  | ||||
| export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { | ||||
|   try { | ||||
|     return !cb(); | ||||
|     const returnValue = cb(); | ||||
|  | ||||
|     if (_.isNil(returnValue)) return true; | ||||
|  | ||||
|     return returnValue === false; | ||||
|   } catch (_) { | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function useValidation<T>({ source, rules }: { source: Ref<T>; rules: UseValidationRule<T>[] }) { | ||||
| export type ValidationAttrs = { | ||||
|   feedback: string; | ||||
|   validationStatus: string | undefined; | ||||
| }; | ||||
|  | ||||
| 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'; | ||||
|     isValid: boolean; | ||||
|     attrs: ValidationAttrs; | ||||
|   }>({ | ||||
|     message: '', | ||||
|     status: undefined, | ||||
|     isValid: false, | ||||
|     attrs: { | ||||
|       validationStatus: undefined, | ||||
|       feedback: '', | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   watch([source], () => { | ||||
|     state.message = ''; | ||||
|     state.status = undefined; | ||||
|   watch( | ||||
|     [source, ...watchRefs], | ||||
|     () => { | ||||
|       state.message = ''; | ||||
|       state.status = undefined; | ||||
|  | ||||
|     for (const rule of rules) { | ||||
|       if (isFalsyOrHasThrown(() => rule.validator(source.value))) { | ||||
|         state.message = rule.message; | ||||
|         state.status = 'error'; | ||||
|       for (const rule of rules) { | ||||
|         if (isFalsyOrHasThrown(() => rule.validator(source.value))) { | ||||
|           state.message = rule.message; | ||||
|           state.status = 'error'; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|       state.isValid = state.status !== 'error'; | ||||
|       state.attrs.feedback = state.message; | ||||
|       state.attrs.validationStatus = state.status; | ||||
|     }, | ||||
|     { immediate: true }, | ||||
|   ); | ||||
|  | ||||
|   return state; | ||||
| } | ||||
|   | ||||
| @@ -29,6 +29,12 @@ export const config = figue({ | ||||
|     }, | ||||
|   }, | ||||
|   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({ | ||||
|   | ||||
| @@ -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 SearchBar from '../components/SearchBar.vue'; | ||||
| import { useStyleStore } from '@/stores/style.store'; | ||||
| import { config } from '@/config'; | ||||
| import type { ToolCategory } from '@/tools/tools.types'; | ||||
| import { useToolStore } from '@/tools/tools.store'; | ||||
| import { useTracker } from '@/modules/tracker/tracker.services'; | ||||
| import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue'; | ||||
| import SearchBar from '../components/SearchBar.vue'; | ||||
| import HeroGradient from '../assets/hero-gradient.svg?component'; | ||||
| import MenuLayout from '../components/MenuLayout.vue'; | ||||
| import NavbarButtons from '../components/NavbarButtons.vue'; | ||||
| import { config } from '@/config'; | ||||
| import MenuIconItem from '@/components/MenuIconItem.vue'; | ||||
| import type { ITool } from '@/tools/tool'; | ||||
|  | ||||
| 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,14 +47,7 @@ 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> | ||||
| @@ -134,23 +123,27 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({ | ||||
|  | ||||
|         <search-bar /> | ||||
|  | ||||
|         <navbar-buttons v-if="!styleStore.isSmallScreen" /> | ||||
|  | ||||
|         <n-tooltip trigger="hover"> | ||||
|           <template #trigger> | ||||
|             <n-button | ||||
|               round | ||||
|               type="primary" | ||||
|               tag="a" | ||||
|               href="https://github.com/sponsors/CorentinTh" | ||||
|               href="https://www.buymeacoffee.com/cthmsst" | ||||
|               rel="noopener" | ||||
|               target="_blank" | ||||
|               class="support-button" | ||||
|               :bordered="false" | ||||
|               @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" | ||||
|             > | ||||
|               <n-icon v-if="!styleStore.isSmallScreen" :component="Heart" style="margin-right: 5px" /> | ||||
|               Sponsor | ||||
|               Buy me a coffee | ||||
|               <n-icon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 /> | ||||
|             </n-button> | ||||
|           </template> | ||||
|           ❤ Support IT Tools development ! | ||||
|         </n-tooltip> | ||||
|  | ||||
|         <navbar-buttons v-if="!styleStore.isSmallScreen" /> | ||||
|       </div> | ||||
|       <slot /> | ||||
|     </template> | ||||
| @@ -169,6 +162,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; | ||||
|   transition: all ease 0.2s; | ||||
|  | ||||
|   &:hover { | ||||
|     color: #fff; | ||||
|     padding-left: 30px; | ||||
|     padding-right: 30px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .footer { | ||||
|   text-align: center; | ||||
|   color: #838587; | ||||
|   | ||||
| @@ -1,24 +1,24 @@ | ||||
| <script lang="ts" setup> | ||||
| import { useRoute } from 'vue-router'; | ||||
| import BaseLayout from './base.layout.vue'; | ||||
| import { useHead } from '@vueuse/head'; | ||||
| import type { HeadObject } from '@vueuse/head'; | ||||
| import { computed } from 'vue'; | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import 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; | ||||
|     } | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import { createApp } from 'vue'; | ||||
| import { createPinia } from 'pinia'; | ||||
| import { createHead } from '@vueuse/head'; | ||||
| // eslint-disable-next-line import/no-unresolved | ||||
| import { registerSW } from 'virtual:pwa-register'; | ||||
| import { plausible } from './plugins/plausible.plugin'; | ||||
|  | ||||
| import 'virtual:uno.css'; | ||||
|  | ||||
| registerSW(); | ||||
|  | ||||
| import { naive } from './plugins/naive.plugin'; | ||||
|   | ||||
							
								
								
									
										27
									
								
								src/modules/tracker/tracker.services.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/modules/tracker/tracker.services.ts
									
									
									
									
									
										Normal 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, | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/modules/tracker/tracker.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/modules/tracker/tracker.types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import type { createTrackerService } from './tracker.services'; | ||||
|  | ||||
| export type TrackerService = ReturnType<typeof createTrackerService>; | ||||
| @@ -1,23 +1,20 @@ | ||||
| <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> | ||||
|  | ||||
|     <router-link to="/" #="{ navigate, href }" custom> | ||||
|       <n-button tag="a" :href="href" secondary @click="navigate"> Back home </n-button> | ||||
|     </router-link> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped> | ||||
| .e404-wrapper { | ||||
|   padding-top: 150px; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -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> | ||||
| @@ -22,9 +24,10 @@ useHead({ title: 'About - IT Tools' }); | ||||
|         type="primary" | ||||
|         tag="a" | ||||
|         text | ||||
|         href="https://github.com/sponsors/CorentinTh" | ||||
|         href="https://www.buymeacoffee.com/cthmsst" | ||||
|         rel="noopener" | ||||
|         target="_blank" | ||||
|         @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" | ||||
|       > | ||||
|         sponsoring me </n-button | ||||
|       >. | ||||
| @@ -38,7 +41,7 @@ useHead({ title: 'About - IT Tools' }); | ||||
|         type="primary" | ||||
|         tag="a" | ||||
|         text | ||||
|         href="https://github.com/CorentinTh/it-tools/blob/master/package.json" | ||||
|         href="https://github.com/CorentinTh/it-tools/blob/main/package.json" | ||||
|         rel="noopener" | ||||
|         target="_blank" | ||||
|       > | ||||
| @@ -47,7 +50,7 @@ useHead({ title: 'About - IT Tools' }); | ||||
|       file of the repository. | ||||
|     </n-p> | ||||
|  | ||||
|     <n-h2>Found a bug ? A tool is missing ?</n-h2> | ||||
|     <n-h2>Found a bug? A tool is missing?</n-h2> | ||||
|     <n-p> | ||||
|       If you need a tool that is currently not present here, and you think can be relevant, you are welcome to submit a | ||||
|       feature request in the | ||||
| @@ -61,7 +64,7 @@ useHead({ title: 'About - IT Tools' }); | ||||
|       > | ||||
|         issues section | ||||
|       </n-button> | ||||
|       in the github repository. | ||||
|       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 | ||||
| @@ -75,7 +78,7 @@ useHead({ title: 'About - IT Tools' }); | ||||
|       > | ||||
|         issues section | ||||
|       </n-button> | ||||
|       in the github repository. | ||||
|       in the GitHub repository. | ||||
|     </n-p> | ||||
|   </div> | ||||
| </template> | ||||
|   | ||||
| @@ -1,24 +1,72 @@ | ||||
| <script setup lang="ts"> | ||||
| import { toolsWithCategory } from '@/tools'; | ||||
| import ToolCard from '../components/ToolCard.vue'; | ||||
| 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 | ||||
|         v-for="tool in [ | ||||
|           ...toolsWithCategory.filter(({ isNew }) => isNew), | ||||
|           ...toolsWithCategory.filter(({ isNew }) => !isNew), | ||||
|         ]" | ||||
|         :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> | ||||
|  | ||||
| @@ -26,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> | ||||
|   | ||||
| @@ -1,112 +1,3 @@ | ||||
| import { | ||||
|   create, | ||||
|   NButton, | ||||
|   NConfigProvider, | ||||
|   NCard, | ||||
|   NInput, | ||||
|   NColorPicker, | ||||
|   NInputNumber, | ||||
|   NSpace, | ||||
|   NH1, | ||||
|   NForm, | ||||
|   NFormItem, | ||||
|   NTimePicker, | ||||
|   NText, | ||||
|   NIcon, | ||||
|   NSwitch, | ||||
|   NCollapseTransition, | ||||
|   NGrid, | ||||
|   NGridItem, | ||||
|   NPopconfirm, | ||||
|   NSlider, | ||||
|   NCollapse, | ||||
|   NCollapseItem, | ||||
|   NProgress, | ||||
|   NAutoComplete, | ||||
|   NSelect, | ||||
|   NUpload, | ||||
|   NEmpty, | ||||
|   NModal, | ||||
|   NTooltip, | ||||
|   NAlert, | ||||
|   NP, | ||||
|   NH2, | ||||
|   NDropdown, | ||||
|   NLayout, | ||||
|   NLayoutSider, | ||||
|   NMenu, | ||||
|   NMessageProvider, | ||||
|   NPageHeader, | ||||
|   NResult, | ||||
|   NH3, | ||||
|   NEllipsis, | ||||
|   NTag, | ||||
|   NInputGroup, | ||||
|   NInputGroupLabel, | ||||
|   NDivider, | ||||
|   NStatistic, | ||||
|   NTable, | ||||
|   NUploadDragger, | ||||
|   NImage, | ||||
|   NScrollbar, | ||||
|   NGradientText, | ||||
|   NCode, | ||||
|   NDatePicker, | ||||
| } from 'naive-ui'; | ||||
| import { create } from 'naive-ui'; | ||||
|  | ||||
| const components = [ | ||||
|   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(); | ||||
|   | ||||
| @@ -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); | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { layouts } from './layouts/index'; | ||||
| import { createRouter, createWebHistory } from 'vue-router'; | ||||
| import { layouts } from './layouts/index'; | ||||
| import HomePage from './pages/Home.page.vue'; | ||||
| import NotFound from './pages/404.page.vue'; | ||||
| import { tools } from './tools'; | ||||
|   | ||||
| @@ -6,6 +6,12 @@ export const lightThemeOverrides: GlobalThemeOverrides = { | ||||
|   }, | ||||
|  | ||||
|   Layout: { color: '#f1f5f9' }, | ||||
|  | ||||
|   AutoComplete: { | ||||
|     peers: { | ||||
|       InternalSelectMenu: { height: '500px' }, | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const darkThemeOverrides: GlobalThemeOverrides = { | ||||
| @@ -16,18 +22,33 @@ export const darkThemeOverrides: GlobalThemeOverrides = { | ||||
|     primaryColorSuppl: '#36AD6AFF', | ||||
|   }, | ||||
|  | ||||
|   Notification: { | ||||
|     color: '#333333', | ||||
|   }, | ||||
|  | ||||
|   AutoComplete: { | ||||
|     peers: { | ||||
|       InternalSelectMenu: { height: '500px', color: '#1e1e1e' }, | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   Menu: { | ||||
|     itemHeight: '32px', | ||||
|   }, | ||||
|  | ||||
|   Layout: { | ||||
|     color: '#121212', | ||||
|     siderColor: '#1e1e1e', | ||||
|     color: '#1c1c1c', | ||||
|     siderColor: '#232323', | ||||
|     siderBorderColor: 'transparent', | ||||
|   }, | ||||
|  | ||||
|   Card: { | ||||
|     color: '#1e1e1e', | ||||
|     borderColor: 'transparent', | ||||
|     color: '#232323', | ||||
|     borderColor: '#282828', | ||||
|   }, | ||||
|  | ||||
|   Table: { | ||||
|     tdColor: '#232323', | ||||
|     thColor: '#353535', | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -1,23 +1,30 @@ | ||||
| <template> | ||||
|   <n-card title="Text to base64"> | ||||
|     <n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." /> | ||||
|     <n-input :value="textBase64" type="textarea" readonly /> | ||||
|   <n-card title="Base64 to file"> | ||||
|     <n-form-item | ||||
|       :feedback="base64InputValidation.message" | ||||
|       :validation-status="base64InputValidation.status" | ||||
|       :show-label="false" | ||||
|     > | ||||
|       <n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" /> | ||||
|     </n-form-item> | ||||
|     <n-space justify="center"> | ||||
|       <n-button secondary @click="copyTextBase64()"> Copy </n-button> | ||||
|       <n-button :disabled="base64Input === '' || !base64InputValidation.isValid" secondary @click="downloadFile()"> | ||||
|         Download file | ||||
|       </n-button> | ||||
|     </n-space> | ||||
|   </n-card> | ||||
| 
 | ||||
|   <n-card title="File to base64"> | ||||
|     <n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image"> | ||||
|       <n-upload-dragger> | ||||
|         <div 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> | ||||
|       </n-upload-dragger> | ||||
|     </n-upload> | ||||
| 
 | ||||
|     <n-input :value="fileBase64" type="textarea" readonly /> | ||||
|     <n-input :value="fileBase64" type="textarea" readonly placeholder="File in base64 will be here" /> | ||||
|     <n-space justify="center"> | ||||
|       <n-button secondary @click="copyFileBase64()"> Copy </n-button> | ||||
|     </n-space> | ||||
| @@ -26,14 +33,35 @@ | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { useBase64 } from '@vueuse/core'; | ||||
| import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { isValidBase64 } from '@/utils/base64'; | ||||
| import { Upload } from '@vicons/tabler'; | ||||
| import { ref, type Ref } from 'vue'; | ||||
| import { useBase64 } from '@vueuse/core'; | ||||
| import type { UploadFileInfo } from 'naive-ui'; | ||||
| import { ref, type Ref } from 'vue'; | ||||
| 
 | ||||
| const textInput = ref(''); | ||||
| const { base64: textBase64 } = useBase64(textInput); | ||||
| const { copy: copyTextBase64 } = useCopy({ source: textBase64, text: 'Base64 string copied to the clipboard' }); | ||||
| const base64Input = ref(''); | ||||
| const { download } = useDownloadFileFromBase64({ source: base64Input }); | ||||
| const base64InputValidation = useValidation({ | ||||
|   source: base64Input, | ||||
|   rules: [ | ||||
|     { | ||||
|       message: 'Invalid base 64 string', | ||||
|       validator: (value) => isValidBase64(value.trim()), | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
| 
 | ||||
| function downloadFile() { | ||||
|   if (!base64InputValidation.isValid) return; | ||||
| 
 | ||||
|   try { | ||||
|     download(); | ||||
|   } catch (_) { | ||||
|     // | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const fileList = ref(); | ||||
| const fileInput = ref() as Ref<File>; | ||||
| @@ -2,11 +2,10 @@ import { FileDigit } from '@vicons/tabler'; | ||||
| import { defineTool } from '../tool'; | ||||
| 
 | ||||
| export const tool = defineTool({ | ||||
|   name: 'Base64 converter', | ||||
|   path: '/base64-converter', | ||||
|   name: 'Base64 file converter', | ||||
|   path: '/base64-file-converter', | ||||
|   description: "Convert string, files or images into a it's base64 representation.", | ||||
|   keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'], | ||||
|   component: () => import('./base64-converter.vue'), | ||||
|   component: () => import('./base64-file-converter.vue'), | ||||
|   icon: FileDigit, | ||||
|   redirectFrom: ['/file-to-base64', '/base64-string-converter'], | ||||
| }); | ||||
| @@ -0,0 +1,55 @@ | ||||
| <template> | ||||
|   <n-card title="String to base64"> | ||||
|     <n-form-item label="String to encode"> | ||||
|       <n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." rows="5" /> | ||||
|     </n-form-item> | ||||
|  | ||||
|     <n-form-item label="Base64 of string"> | ||||
|       <n-input | ||||
|         :value="base64Output" | ||||
|         type="textarea" | ||||
|         readonly | ||||
|         placeholder="The base64 encoding of your string will be here" | ||||
|         rows="5" | ||||
|       /> | ||||
|     </n-form-item> | ||||
|  | ||||
|     <n-space justify="center"> | ||||
|       <n-button secondary @click="copyTextBase64()"> Copy base64 </n-button> | ||||
|     </n-space> | ||||
|   </n-card> | ||||
|  | ||||
|   <n-card title="Base64 to string"> | ||||
|     <n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs"> | ||||
|       <n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" /> | ||||
|     </n-form-item> | ||||
|  | ||||
|     <n-form-item label="Decoded string"> | ||||
|       <n-input :value="textOutput" type="textarea" readonly placeholder="The decoded string will be here" rows="5" /> | ||||
|     </n-form-item> | ||||
|  | ||||
|     <n-space justify="center"> | ||||
|       <n-button secondary @click="copyText()"> Copy decoded string </n-button> | ||||
|     </n-space> | ||||
|   </n-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { computed, ref } from 'vue'; | ||||
|  | ||||
| const textInput = ref(''); | ||||
| const base64Output = computed(() => textToBase64(textInput.value)); | ||||
| const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' }); | ||||
|  | ||||
| const base64Input = ref(''); | ||||
| const textOutput = computed(() => withDefaultOnError(() => base64ToText(base64Input.value.trim()), '')); | ||||
| const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); | ||||
| const b64Validation = useValidation({ | ||||
|   source: base64Input, | ||||
|   rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }], | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										12
									
								
								src/tools/base64-string-converter/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/base64-string-converter/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import { FileDigit } from '@vicons/tabler'; | ||||
| import { defineTool } from '../tool'; | ||||
|  | ||||
| export const tool = defineTool({ | ||||
|   name: 'Base64 string encoder/decoder', | ||||
|   path: '/base64-string-converter', | ||||
|   description: 'Simply encode and decode string into a their base64 representation.', | ||||
|   keywords: ['base64', 'converter', 'conversion', 'web', 'data', 'format', 'atob', 'btoa'], | ||||
|   component: () => import('./base64-string-converter.vue'), | ||||
|   icon: FileDigit, | ||||
|   redirectFrom: ['/file-to-base64', '/base64-converter'], | ||||
| }); | ||||
							
								
								
									
										49
									
								
								src/tools/basic-auth-generator/basic-auth-generator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/tools/basic-auth-generator/basic-auth-generator.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-form-item label="Username"> | ||||
|       <n-input v-model:value="username" placeholder="Your username..." clearable /> | ||||
|     </n-form-item> | ||||
|     <n-form-item label="Password"> | ||||
|       <n-input | ||||
|         v-model:value="password" | ||||
|         placeholder="Your password..." | ||||
|         type="password" | ||||
|         show-password-on="click" | ||||
|         clearable | ||||
|       /> | ||||
|     </n-form-item> | ||||
|  | ||||
|     <br /> | ||||
|     <n-card> | ||||
|       <n-statistic label="Authorization header:" class="header"> | ||||
|         <n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none"> | ||||
|           {{ header }} | ||||
|         </n-scrollbar> | ||||
|       </n-statistic> | ||||
|     </n-card> | ||||
|     <br /> | ||||
|     <n-space justify="center"> | ||||
|       <n-button secondary @click="copy">Copy header</n-button> | ||||
|     </n-space> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { textToBase64 } from '@/utils/base64'; | ||||
| import { computed, ref } from 'vue'; | ||||
|  | ||||
| const username = ref(''); | ||||
| const password = ref(''); | ||||
| const header = computed(() => `Authorization: Basic ${textToBase64(`${username.value}:${password.value}`)}`); | ||||
|  | ||||
| const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' }); | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| ::v-deep(.n-statistic-value__content) { | ||||
|   font-family: monospace; | ||||
|   font-size: 17px !important; | ||||
|   white-space: nowrap; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										21
									
								
								src/tools/basic-auth-generator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/tools/basic-auth-generator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { PasswordRound } from '@vicons/material'; | ||||
| import { defineTool } from '../tool'; | ||||
|  | ||||
| export const tool = defineTool({ | ||||
|   name: 'Basic auth generator', | ||||
|   path: '/basic-auth-generator', | ||||
|   description: 'Generate a base64 basic auth header from an username and a password.', | ||||
|   keywords: [ | ||||
|     'basic', | ||||
|     'auth', | ||||
|     'generator', | ||||
|     'username', | ||||
|     'password', | ||||
|     'base64', | ||||
|     'authentication', | ||||
|     'header', | ||||
|     'authorization', | ||||
|   ], | ||||
|   component: () => import('./basic-auth-generator.vue'), | ||||
|   icon: PasswordRound, | ||||
| }); | ||||
| @@ -12,7 +12,7 @@ | ||||
|         /> | ||||
|       </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> | ||||
|   | ||||
							
								
								
									
										34
									
								
								src/tools/benchmark-builder/benchmark-builder.models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/tools/benchmark-builder/benchmark-builder.models.ts
									
									
									
									
									
										Normal 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}`; | ||||
| } | ||||
							
								
								
									
										162
									
								
								src/tools/benchmark-builder/benchmark-builder.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								src/tools/benchmark-builder/benchmark-builder.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,162 @@ | ||||
| <template> | ||||
|   <n-scrollbar style="flex: 1" x-scrollable> | ||||
|     <n-space :wrap="false" style="flex: 1" justify="center" :size="0"> | ||||
|       <div v-for="(suite, index) of suites" :key="index"> | ||||
|         <n-card style="width: 292px; margin: 0 8px 5px"> | ||||
|           <n-form-item label="Suite name:" :show-feedback="false" label-placement="left"> | ||||
|             <n-input v-model:value="suite.title" placeholder="Suite name..." /> | ||||
|           </n-form-item> | ||||
|  | ||||
|           <n-divider></n-divider> | ||||
|           <n-form-item label="Suite values" :show-feedback="false"> | ||||
|             <dynamic-values v-model:values="suite.data" /> | ||||
|           </n-form-item> | ||||
|         </n-card> | ||||
|  | ||||
|         <n-space justify="center"> | ||||
|           <n-button v-if="suites.length > 1" quaternary @click="suites.splice(index, 1)"> | ||||
|             <template #icon> | ||||
|               <n-icon :component="Trash" depth="3" /> | ||||
|             </template> | ||||
|             Delete suite | ||||
|           </n-button> | ||||
|           <n-button quaternary @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })"> | ||||
|             <template #icon> | ||||
|               <n-icon :component="Plus" depth="3" /> | ||||
|             </template> | ||||
|             Add suite | ||||
|           </n-button> | ||||
|         </n-space> | ||||
|       </div> | ||||
|     </n-space> | ||||
|     <br /> | ||||
|   </n-scrollbar> | ||||
|  | ||||
|   <div style="flex: 0 0 100%"> | ||||
|     <div style="max-width: 600px; margin: 0 auto"> | ||||
|       <n-space justify="center"> | ||||
|         <n-form-item label="Unit:" label-placement="left"> | ||||
|           <n-input v-model:value="unit" placeholder="Unit (eg: ms)" /> | ||||
|         </n-form-item> | ||||
|  | ||||
|         <n-button | ||||
|           tertiary | ||||
|           @click=" | ||||
|             suites = [ | ||||
|               { title: 'Suite 1', data: [] }, | ||||
|               { title: 'Suite 2', data: [] }, | ||||
|             ] | ||||
|           " | ||||
|           >Reset suites</n-button | ||||
|         > | ||||
|       </n-space> | ||||
|  | ||||
|       <n-table> | ||||
|         <thead> | ||||
|           <tr> | ||||
|             <th>{{ header.position }}</th> | ||||
|             <th>{{ header.title }}</th> | ||||
|             <th>{{ header.size }}</th> | ||||
|             <th>{{ header.mean }}</th> | ||||
|             <th>{{ header.variance }}</th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           <tr v-for="{ title, size, mean, variance, position } of results" :key="title"> | ||||
|             <td>{{ position }}</td> | ||||
|             <td>{{ title }}</td> | ||||
|             <td>{{ size }}</td> | ||||
|             <td>{{ mean }}</td> | ||||
|             <td>{{ variance }}</td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </n-table> | ||||
|       <br /> | ||||
|       <n-space justify="center"> | ||||
|         <n-button tertiary @click="copyAsMarkdown">Copy as markdown table</n-button> | ||||
|         <n-button tertiary @click="copyAsBulletList">Copy as bullet list</n-button> | ||||
|       </n-space> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { Trash, Plus } from '@vicons/tabler'; | ||||
| import { useClipboard, useStorage } from '@vueuse/core'; | ||||
| import _ 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> | ||||
							
								
								
									
										61
									
								
								src/tools/benchmark-builder/dynamic-values.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/tools/benchmark-builder/dynamic-values.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-space v-for="(value, index) of values" :key="index" :wrap="false" style="margin-bottom: 5px" :size="5"> | ||||
|       <n-input-number | ||||
|         :ref="refs.set" | ||||
|         v-model:value="values[index]" | ||||
|         :show-button="false" | ||||
|         placeholder="Set your measure..." | ||||
|         autofocus | ||||
|         @keydown.enter="onInputEnter(index)" | ||||
|       /> | ||||
|       <n-tooltip> | ||||
|         <template #trigger> | ||||
|           <n-button circle quaternary @click="values.splice(index, 1)"> | ||||
|             <template #icon> | ||||
|               <n-icon :component="Trash" depth="3" /> | ||||
|             </template> | ||||
|           </n-button> | ||||
|         </template> | ||||
|         Delete value | ||||
|       </n-tooltip> | ||||
|     </n-space> | ||||
|  | ||||
|     <n-button tertiary @click="addValue"> | ||||
|       <template #icon> | ||||
|         <n-icon :component="Plus" /> | ||||
|       </template> | ||||
|       Add a measure | ||||
|     </n-button> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { Trash, Plus } from '@vicons/tabler'; | ||||
| import { 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> | ||||
							
								
								
									
										12
									
								
								src/tools/benchmark-builder/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/benchmark-builder/index.ts
									
									
									
									
									
										Normal 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'), | ||||
| }); | ||||
| @@ -59,24 +59,26 @@ | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { ref, computed } from 'vue'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { isNotThrowing } from '@/utils/boolean'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { | ||||
|   entropyToMnemonic, | ||||
|   englishWordList, | ||||
|   chineseSimplifiedWordList, | ||||
|   chineseTraditionalWordList, | ||||
|   czechWordList, | ||||
|   englishWordList, | ||||
|   entropyToMnemonic, | ||||
|   frenchWordList, | ||||
|   generateEntropy, | ||||
|   italianWordList, | ||||
|   japaneseWordList, | ||||
|   koreanWordList, | ||||
|   mnemonicToEntropy, | ||||
|   portugueseWordList, | ||||
|   spanishWordList, | ||||
|   generateEntropy, | ||||
|   mnemonicToEntropy, | ||||
| } from '@it-tools/bip39'; | ||||
| import { Copy, Refresh } from '@vicons/tabler'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { computed, ref } from 'vue'; | ||||
|  | ||||
| const languages = { | ||||
|   English: englishWordList, | ||||
| @@ -97,20 +99,11 @@ const passphraseInput = ref(''); | ||||
| const language = ref<keyof typeof languages>('English'); | ||||
| const passphrase = computed({ | ||||
|   get() { | ||||
|     try { | ||||
|       return entropyToMnemonic(entropy.value, languages[language.value]); | ||||
|     } catch (_) { | ||||
|       return passphraseInput.value; | ||||
|     } | ||||
|     return withDefaultOnError(() => entropyToMnemonic(entropy.value, languages[language.value]), passphraseInput.value); | ||||
|   }, | ||||
|   set(value: string) { | ||||
|     passphraseInput.value = value; | ||||
|  | ||||
|     try { | ||||
|       entropy.value = mnemonicToEntropy(value, languages[language.value]); | ||||
|     } catch (_) { | ||||
|       entropy.value = ''; | ||||
|     } | ||||
|     entropy.value = withDefaultOnError(() => mnemonicToEntropy(value, languages[language.value]), ''); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| @@ -123,7 +116,7 @@ const entropyValidation = useValidation({ | ||||
|     }, | ||||
|     { | ||||
|       validator: (value) => /^[a-fA-F0-9]*$/.test(value), | ||||
|       message: 'Entropy should an hexadecimal number', | ||||
|       message: 'Entropy should be an hexadecimal string', | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
| @@ -132,14 +125,7 @@ const mnemonicValidation = useValidation({ | ||||
|   source: passphrase, | ||||
|   rules: [ | ||||
|     { | ||||
|       validator: (value) => { | ||||
|         try { | ||||
|           mnemonicToEntropy(value); | ||||
|           return true; | ||||
|         } catch (_) { | ||||
|           return false; | ||||
|         } | ||||
|       }, | ||||
|       validator: (value) => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])), | ||||
|       message: 'Invalid mnemonic', | ||||
|     }, | ||||
|   ], | ||||
|   | ||||
| @@ -8,37 +8,37 @@ | ||||
|       <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> | ||||
| @@ -46,8 +46,6 @@ | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { ref } from 'vue'; | ||||
| import InputCopyable from '../../components/InputCopyable.vue'; | ||||
|  | ||||
| import { | ||||
|   camelCase, | ||||
|   capitalCase, | ||||
| @@ -61,6 +59,11 @@ import { | ||||
|   sentenceCase, | ||||
|   snakeCase, | ||||
| } 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> | ||||
|   | ||||
							
								
								
									
										68
									
								
								src/tools/chmod-calculator/chmod-calculator.service.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/tools/chmod-calculator/chmod-calculator.service.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| import { expect, describe, it } from 'vitest'; | ||||
| import { computeChmodOctalRepresentation } from './chmod-calculator.service'; | ||||
|  | ||||
| describe('chmod-calculator', () => { | ||||
|   describe('computeChmodOctalRepresentation', () => { | ||||
|     it('get the octal representation from permissions', () => { | ||||
|       expect( | ||||
|         computeChmodOctalRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: true, write: true, execute: true }, | ||||
|             group: { read: true, write: true, execute: true }, | ||||
|             public: { read: true, write: true, execute: true }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('777'); | ||||
|  | ||||
|       expect( | ||||
|         computeChmodOctalRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: false, write: false, execute: false }, | ||||
|             group: { read: false, write: false, execute: false }, | ||||
|             public: { read: false, write: false, execute: false }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('000'); | ||||
|  | ||||
|       expect( | ||||
|         computeChmodOctalRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: false, write: true, execute: false }, | ||||
|             group: { read: false, write: true, execute: true }, | ||||
|             public: { read: true, write: false, execute: true }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('235'); | ||||
|  | ||||
|       expect( | ||||
|         computeChmodOctalRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: true, write: false, execute: false }, | ||||
|             group: { read: false, write: true, execute: false }, | ||||
|             public: { read: false, write: false, execute: true }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('421'); | ||||
|  | ||||
|       expect( | ||||
|         computeChmodOctalRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: false, write: false, execute: true }, | ||||
|             group: { read: false, write: true, execute: false }, | ||||
|             public: { read: true, write: false, execute: false }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('124'); | ||||
|  | ||||
|       expect( | ||||
|         computeChmodOctalRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: false, write: true, execute: false }, | ||||
|             group: { read: false, write: true, execute: false }, | ||||
|             public: { read: false, write: true, execute: false }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('222'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										17
									
								
								src/tools/chmod-calculator/chmod-calculator.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/tools/chmod-calculator/chmod-calculator.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import _ from 'lodash'; | ||||
| import type { GroupPermissions, Permissions } from './chmod-calculator.types'; | ||||
|  | ||||
| export { computeChmodOctalRepresentation }; | ||||
|  | ||||
| function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string { | ||||
|   const permissionValue = { read: 4, write: 2, execute: 1 }; | ||||
|  | ||||
|   const getGroupPermissionValue = (permission: GroupPermissions) => | ||||
|     _.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, 0) : 0), 0); | ||||
|  | ||||
|   return [ | ||||
|     getGroupPermissionValue(permissions.owner), | ||||
|     getGroupPermissionValue(permissions.group), | ||||
|     getGroupPermissionValue(permissions.public), | ||||
|   ].join(''); | ||||
| } | ||||
							
								
								
									
										10
									
								
								src/tools/chmod-calculator/chmod-calculator.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/tools/chmod-calculator/chmod-calculator.types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| export type Scope = 'read' | 'write' | 'execute'; | ||||
| export type Group = 'owner' | 'group' | 'public'; | ||||
|  | ||||
| export type GroupPermissions = { | ||||
|   [k in Scope]: boolean; | ||||
| }; | ||||
|  | ||||
| export type Permissions = { | ||||
|   [k in Group]: GroupPermissions; | ||||
| }; | ||||
							
								
								
									
										83
									
								
								src/tools/chmod-calculator/chmod-calculator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/tools/chmod-calculator/chmod-calculator.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table"> | ||||
|       <thead> | ||||
|         <tr> | ||||
|           <th class="text-center" scope="col"></th> | ||||
|           <th class="text-center" scope="col">Owner (u)</th> | ||||
|           <th class="text-center" scope="col">Group (g)</th> | ||||
|           <th class="text-center" scope="col">Public (o)</th> | ||||
|         </tr> | ||||
|       </thead> | ||||
|       <tbody> | ||||
|         <tr v-for="{ scope, title } of scopes" :key="scope"> | ||||
|           <td class="line-header">{{ title }}</td> | ||||
|           <td v-for="group of groups" :key="group" class="text-center"> | ||||
|             <!-- <n-switch v-model:value="permissions[group][scope]" /> --> | ||||
|             <n-checkbox v-model:checked="permissions[group][scope]" size="large" /> | ||||
|           </td> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|     </n-table> | ||||
|  | ||||
|     <div class="octal-result"> | ||||
|       {{ octal }} | ||||
|     </div> | ||||
|  | ||||
|     <input-copyable :value="`chmod ${octal} path`" readonly /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import { computed, ref } from 'vue'; | ||||
| import { computeChmodOctalRepresentation } from './chmod-calculator.service'; | ||||
| import InputCopyable from '../../components/InputCopyable.vue'; | ||||
|  | ||||
| import type { Group, Scope } from './chmod-calculator.types'; | ||||
|  | ||||
| const themeVars = useThemeVars(); | ||||
|  | ||||
| const scopes: { scope: Scope; title: string }[] = [ | ||||
|   { scope: 'read', title: 'Read (4)' }, | ||||
|   { scope: 'write', title: 'Write (2)' }, | ||||
|   { scope: 'execute', title: 'Execute (1)' }, | ||||
| ]; | ||||
| const groups: Group[] = ['owner', 'group', 'public']; | ||||
|  | ||||
| const permissions = ref({ | ||||
|   owner: { read: false, write: false, execute: false }, | ||||
|   group: { read: false, write: false, execute: false }, | ||||
|   public: { read: false, write: false, execute: false }, | ||||
| }); | ||||
|  | ||||
| const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value })); | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .octal-result { | ||||
|   text-align: center; | ||||
|   font-size: 50px; | ||||
|   font-family: monospace; | ||||
|   color: v-bind('themeVars.primaryColor'); | ||||
|   margin: 20px 0; | ||||
| } | ||||
| .permission-table { | ||||
|   td, | ||||
|   th { | ||||
|     padding: 15px; | ||||
|  | ||||
|     @media screen and (max-width: 600px) { | ||||
|       padding: 5px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| .line-header { | ||||
|   font-weight: bold; | ||||
|   text-align: right; | ||||
|   max-width: 80px; | ||||
| } | ||||
| .text-center { | ||||
|   text-align: center; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										22
									
								
								src/tools/chmod-calculator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/tools/chmod-calculator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import { FileInvoice } from '@vicons/tabler'; | ||||
| import { defineTool } from '../tool'; | ||||
|  | ||||
| export const tool = defineTool({ | ||||
|   name: 'Chmod calculator', | ||||
|   path: '/chmod-calculator', | ||||
|   description: 'Compute your chmod permissions and commands with this online chmod calculator.', | ||||
|   keywords: [ | ||||
|     'chmod', | ||||
|     'calculator', | ||||
|     'file', | ||||
|     'permission', | ||||
|     'files', | ||||
|     'directory', | ||||
|     'folder', | ||||
|     'recursive', | ||||
|     'generator', | ||||
|     'octal', | ||||
|   ], | ||||
|   component: () => import('./chmod-calculator.vue'), | ||||
|   icon: FileInvoice, | ||||
| }); | ||||
| @@ -1,12 +1,13 @@ | ||||
| import { describe, expect, it } from 'vitest'; | ||||
| import { formatChronometerTime } from './chronometer.service'; | ||||
| import { formatMs } from './chronometer.service'; | ||||
|  | ||||
| describe('chronometer', () => { | ||||
|   describe('formatChronometerTime', () => { | ||||
|     it('format the elapsed time', () => { | ||||
|       expect(formatChronometerTime({ elapsed: 123456 })).toEqual('02:03.456'); | ||||
|       expect(formatChronometerTime({ elapsed: 123456, msPerUnit: 100 })).toEqual('03:25:45.600'); | ||||
|       expect(formatChronometerTime({ elapsed: 12345600 })).toEqual('03:25:45.600'); | ||||
|       expect(formatMs(0)).toEqual('00:00.000'); | ||||
|       expect(formatMs(1)).toEqual('00:00.001'); | ||||
|       expect(formatMs(123456)).toEqual('02:03.456'); | ||||
|       expect(formatMs(12345600)).toEqual('03:25:45.600'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,10 +1,8 @@ | ||||
| export function formatChronometerTime({ elapsed, msPerUnit = 1 }: { elapsed: number; msPerUnit?: number }) { | ||||
|   const elapsedMs = elapsed * msPerUnit; | ||||
|  | ||||
|   const ms = elapsedMs % 1000; | ||||
|   const secs = ((elapsedMs - ms) / 1000) % 60; | ||||
|   const mins = (((elapsedMs - ms) / 1000 - secs) / 60) % 60; | ||||
|   const hrs = (((elapsedMs - ms) / 1000 - secs) / 60 - mins) / 60; | ||||
| export function formatMs(msTotal: number) { | ||||
|   const ms = msTotal % 1000; | ||||
|   const secs = ((msTotal - ms) / 1000) % 60; | ||||
|   const mins = (((msTotal - ms) / 1000 - secs) / 60) % 60; | ||||
|   const hrs = (((msTotal - ms) / 1000 - secs) / 60 - mins) / 60; | ||||
|   const hrsString = hrs > 0 ? `${hrs.toString().padStart(2, '0')}:` : ''; | ||||
|  | ||||
|   return `${hrsString}${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-card> | ||||
|       <div class="duration">{{ formatChronometerTime({ elapsed: counter, msPerUnit }) }}</div> | ||||
|       <div class="duration">{{ formatMs(counter) }}</div> | ||||
|     </n-card> | ||||
|     <br /> | ||||
|     <n-space justify="center"> | ||||
|       <n-button v-if="!isActive" secondary type="primary" @click="resume">Start</n-button> | ||||
|       <n-button v-if="!isRunning" secondary type="primary" @click="resume">Start</n-button> | ||||
|       <n-button v-else secondary type="warning" @click="pause">Stop</n-button> | ||||
|  | ||||
|       <n-button secondary @click="counter = 0">Reset</n-button> | ||||
| @@ -14,12 +14,33 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useInterval } from '@vueuse/core'; | ||||
| import { formatChronometerTime } from './chronometer.service'; | ||||
| import { useRafFn } from '@vueuse/core'; | ||||
| import { ref } from 'vue'; | ||||
| import { formatMs } from './chronometer.service'; | ||||
|  | ||||
| const msPerUnit = 10; | ||||
| const isRunning = ref(false); | ||||
| const counter = ref(0); | ||||
|  | ||||
| const { counter, pause, resume, isActive } = useInterval(msPerUnit, { controls: true, immediate: false }); | ||||
| let previousRafDate = Date.now(); | ||||
| const { pause: pauseRaf, resume: resumeRaf } = useRafFn( | ||||
|   () => { | ||||
|     const deltaMs = Date.now() - previousRafDate; | ||||
|     previousRafDate = Date.now(); | ||||
|     counter.value += deltaMs; | ||||
|   }, | ||||
|   { immediate: false }, | ||||
| ); | ||||
|  | ||||
| function resume() { | ||||
|   previousRafDate = Date.now(); | ||||
|   resumeRaf(); | ||||
|   isRunning.value = true; | ||||
| } | ||||
|  | ||||
| function pause() { | ||||
|   pauseRaf(); | ||||
|   isRunning.value = false; | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
|   | ||||
| @@ -36,12 +36,12 @@ | ||||
| <script setup lang="ts"> | ||||
| import { ref } from 'vue'; | ||||
| import { colord, extend } from 'colord'; | ||||
| import InputCopyable from '../../components/InputCopyable.vue'; | ||||
|  | ||||
| import cmykPlugin from 'colord/plugins/cmyk'; | ||||
| import hwbPlugin from 'colord/plugins/hwb'; | ||||
| import namesPlugin from 'colord/plugins/names'; | ||||
| import lchPlugin from 'colord/plugins/lch'; | ||||
| import InputCopyable from '../../components/InputCopyable.vue'; | ||||
|  | ||||
| extend([cmykPlugin, hwbPlugin, namesPlugin, lchPlugin]); | ||||
|  | ||||
|   | ||||
| @@ -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'); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										142
									
								
								src/tools/date-time-converter/date-time-converter.models.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/tools/date-time-converter/date-time-converter.models.test.ts
									
									
									
									
									
										Normal 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); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										46
									
								
								src/tools/date-time-converter/date-time-converter.models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/tools/date-time-converter/date-time-converter.models.ts
									
									
									
									
									
										Normal 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; | ||||
|   } | ||||
| } | ||||
| @@ -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; | ||||
| }; | ||||
| @@ -1,44 +1,38 @@ | ||||
| <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" | ||||
|           :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,90 +41,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> | ||||
|   | ||||
| @@ -1,16 +1,17 @@ | ||||
| <template> | ||||
|   <n-card v-for="{ name, information } in sections" :key="name" :title="name"> | ||||
|     <n-grid cols="1 400:2" x-gap="12" y-gap="12"> | ||||
|       <n-gi v-for="{ label, value } in information" :key="label" class="information"> | ||||
|       <n-gi v-for="{ label, value: { value } } in information" :key="label" class="information"> | ||||
|         <n-card :bordered="false" embedded> | ||||
|           <div class="label"> | ||||
|             {{ label }} | ||||
|           </div> | ||||
|  | ||||
|           <div class="value"> | ||||
|             <n-ellipsis> | ||||
|               {{ value.value }} | ||||
|             <n-ellipsis v-if="value"> | ||||
|               {{ value }} | ||||
|             </n-ellipsis> | ||||
|             <div v-else class="undefined-value">unknown</div> | ||||
|           </div> | ||||
|         </n-card> | ||||
|       </n-gi> | ||||
| @@ -89,7 +90,10 @@ const sections = [ | ||||
|   .value { | ||||
|     font-size: 20px; | ||||
|     font-weight: 400; | ||||
|     line-height: 1; | ||||
|   } | ||||
|  | ||||
|   .undefined-value { | ||||
|     opacity: 0.8; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										4
									
								
								src/tools/docker-run-to-docker-compose-converter/composerize.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/tools/docker-run-to-docker-compose-converter/composerize.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| declare module 'composerize' { | ||||
|   const composerize: (arg: string) => string; | ||||
|   export default composerize; | ||||
| } | ||||
| @@ -0,0 +1,84 @@ | ||||
| <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" /> | ||||
|     <br /> | ||||
|     <br /> | ||||
|     <n-space justify="center"> | ||||
|       <n-button :disabled="dockerCompose === ''" secondary @click="download"> Download docker-compose.yml </n-button> | ||||
|     </n-space> | ||||
|  | ||||
|     <div v-if="notComposable.length > 0"> | ||||
|       <br /> | ||||
|       <n-alert title="This options are not translatable to docker-compose" type="info"> | ||||
|         <ul> | ||||
|           <li v-for="(message, index) of notComposable" :key="index">{{ message }}</li> | ||||
|         </ul> | ||||
|       </n-alert> | ||||
|     </div> | ||||
|  | ||||
|     <div v-if="notImplemented.length > 0"> | ||||
|       <br /> | ||||
|       <n-alert | ||||
|         title="This options are not yet implemented and therefore haven't been translated to docker-compose" | ||||
|         type="warning" | ||||
|       > | ||||
|         <ul> | ||||
|           <li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li> | ||||
|         </ul> | ||||
|       </n-alert> | ||||
|     </div> | ||||
|  | ||||
|     <div v-if="errors.length > 0"> | ||||
|       <br /> | ||||
|       <n-alert title="The following errors occured" type="error"> | ||||
|         <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> | ||||
							
								
								
									
										11
									
								
								src/tools/docker-run-to-docker-compose-converter/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/tools/docker-run-to-docker-compose-converter/index.ts
									
									
									
									
									
										Normal 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, | ||||
| }); | ||||
| @@ -11,7 +11,7 @@ | ||||
|       <div> | ||||
|         <n-space item-style="flex:1 1 0"> | ||||
|           <n-form-item label="Amount of element to consume"> | ||||
|             <n-input-number v-model:value="unitCount" /> | ||||
|             <n-input-number v-model:value="unitCount" :min="1" /> | ||||
|           </n-form-item> | ||||
|           <n-form-item label="The consumption started at"> | ||||
|             <n-date-picker v-model:value="startedAt" type="datetime" /> | ||||
| @@ -19,10 +19,10 @@ | ||||
|         </n-space> | ||||
|  | ||||
|         <n-form-item label="Amount of unit consumed by time span" :show-feedback="false"> | ||||
|           <n-input-number v-model:value="unitPerTimeSpan" /> | ||||
|           <n-input-number v-model:value="unitPerTimeSpan" :min="1" /> | ||||
|           <span style="margin: 0 10px">in</span> | ||||
|           <n-input-group> | ||||
|             <n-input-number v-model:value="timeSpan" /> | ||||
|             <n-input-number v-model:value="timeSpan" :min="1" /> | ||||
|             <n-select | ||||
|               v-model:value="timeSpanUnitMultiplier" | ||||
|               :options="[ | ||||
| @@ -51,7 +51,10 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| // Duplicate issue with sub directory | ||||
| // eslint-disable-next-line import/no-duplicates | ||||
| import { addMilliseconds, formatRelative } from 'date-fns'; | ||||
| // eslint-disable-next-line import/no-duplicates | ||||
| import { enGB } from 'date-fns/locale'; | ||||
| import { computed, ref } from 'vue'; | ||||
| import { formatMsDuration } from './eta-calculator.service'; | ||||
|   | ||||
| @@ -5,8 +5,8 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import Memo from './git-memo.md'; | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import Memo from './git-memo.md'; | ||||
|  | ||||
| const themeVars = useThemeVars(); | ||||
| </script> | ||||
|   | ||||
							
								
								
									
										15
									
								
								src/tools/hash-text/hash-text.service.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/tools/hash-text/hash-text.service.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import { describe, expect, it } from 'vitest'; | ||||
| import { convertHexToBin } from './hash-text.service'; | ||||
|  | ||||
| describe('hash text', () => { | ||||
|   describe('convertHexToBin', () => { | ||||
|     it('convert hex to bin', () => { | ||||
|       expect(convertHexToBin('')).toEqual(''); | ||||
|       expect(convertHexToBin('FF')).toEqual('11111111'); | ||||
|       expect(convertHexToBin('F'.repeat(200))).toEqual('1111'.repeat(200)); | ||||
|       expect(convertHexToBin('2123006AD00F694CE120')).toEqual( | ||||
|         '00100001001000110000000001101010110100000000111101101001010011001110000100100000', | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										7
									
								
								src/tools/hash-text/hash-text.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/tools/hash-text/hash-text.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| export function convertHexToBin(hex: string) { | ||||
|   return hex | ||||
|     .trim() | ||||
|     .split('') | ||||
|     .map((byte) => parseInt(byte, 16).toString(2).padStart(4, '0')) | ||||
|     .join(''); | ||||
| } | ||||
| @@ -1,10 +1,34 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-card> | ||||
|       <n-input v-model:value="clearText" type="textarea" placeholder="Your string..." :autosize="{ minRows: 3 }" /> | ||||
|       <n-input v-model:value="clearText" type="textarea" placeholder="Your string to hash..." rows="3" /> | ||||
|  | ||||
|       <n-divider /> | ||||
|  | ||||
|       <n-form-item label="Digest encoding"> | ||||
|         <n-select | ||||
|           v-model:value="encoding" | ||||
|           :options="[ | ||||
|             { | ||||
|               label: 'Binary (base 2)', | ||||
|               value: 'Bin', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Hexadecimal (base 16)', | ||||
|               value: 'Hex', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Base64 (base 64)', | ||||
|               value: 'Base64', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Base64url (base 64 with url safe chars)', | ||||
|               value: 'Base64url', | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </n-form-item> | ||||
|  | ||||
|       <div v-for="algo in algoNames" :key="algo" style="margin: 5px 0"> | ||||
|         <n-input-group> | ||||
|           <n-input-group-label style="flex: 0 0 120px"> {{ algo }} </n-input-group-label> | ||||
| @@ -16,9 +40,11 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import InputCopyable from '../../components/InputCopyable.vue'; | ||||
| import { useQueryParam } from '@/composable/queryParams'; | ||||
| import { enc, lib, MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512 } from 'crypto-js'; | ||||
| import { ref } from 'vue'; | ||||
| import { MD5, SHA1, SHA256, SHA224, SHA512, SHA384, SHA3, RIPEMD160 } from 'crypto-js'; | ||||
| import InputCopyable from '../../components/InputCopyable.vue'; | ||||
| import { convertHexToBin } from './hash-text.service'; | ||||
|  | ||||
| const algos = { | ||||
|   MD5, | ||||
| @@ -32,10 +58,18 @@ const algos = { | ||||
| } as const; | ||||
|  | ||||
| type AlgoNames = keyof typeof algos; | ||||
| type Encoding = keyof typeof enc | 'Bin'; | ||||
| const algoNames = Object.keys(algos) as AlgoNames[]; | ||||
| const encoding = useQueryParam<Encoding>({ defaultValue: 'Hex', name: 'encoding' }); | ||||
| const clearText = ref(''); | ||||
|  | ||||
| const clearText = ref( | ||||
|   'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lacus metus blandit dolor lacus natoque ad fusce aliquam velit.', | ||||
| ); | ||||
| const hashText = (algo: AlgoNames, value: string) => algos[algo](value).toString(); | ||||
| function formatWithEncoding(words: lib.WordArray, encoding: Encoding) { | ||||
|   if (encoding === 'Bin') { | ||||
|     return convertHexToBin(words.toString(enc.Hex)); | ||||
|   } | ||||
|  | ||||
|   return words.toString(enc[encoding]); | ||||
| } | ||||
|  | ||||
| const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[algo](value), encoding.value); | ||||
| </script> | ||||
|   | ||||
							
								
								
									
										96
									
								
								src/tools/hmac-generator/hmac-generator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/tools/hmac-generator/hmac-generator.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-form-item label="Plain text to compute the hash"> | ||||
|       <n-input v-model:value="plainText" type="textarea" placeholder="Enter the text to compute the hash..." /> | ||||
|     </n-form-item> | ||||
|     <n-form-item label="Secret key"> | ||||
|       <n-input v-model:value="secret" placeholder="Enter the secret key..." /> | ||||
|     </n-form-item> | ||||
|     <n-space item-style="flex:1 1 0"> | ||||
|       <n-form-item label="Hashing function"> | ||||
|         <n-select | ||||
|           v-model:value="hashFunction" | ||||
|           placeholder="Select an hashing function..." | ||||
|           :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||
|         /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Output encoding"> | ||||
|         <n-select | ||||
|           v-model:value="encoding" | ||||
|           placeholder="Select the result encoding..." | ||||
|           :options="[ | ||||
|             { | ||||
|               label: 'Binary (base 2)', | ||||
|               value: 'Bin', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Hexadecimal (base 16)', | ||||
|               value: 'Hex', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Base64 (base 64)', | ||||
|               value: 'Base64', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Base64-url (base 64 with url safe chars)', | ||||
|               value: 'Base64url', | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </n-form-item> | ||||
|     </n-space> | ||||
|     <n-form-item label="HMAC of your text"> | ||||
|       <n-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." /> | ||||
|     </n-form-item> | ||||
|     <n-space justify="center"> | ||||
|       <n-button secondary @click="copy()">Copy HMAC</n-button> | ||||
|     </n-space> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { | ||||
|   enc, | ||||
|   HmacMD5, | ||||
|   HmacRIPEMD160, | ||||
|   HmacSHA1, | ||||
|   HmacSHA224, | ||||
|   HmacSHA256, | ||||
|   HmacSHA3, | ||||
|   HmacSHA384, | ||||
|   HmacSHA512, | ||||
|   lib, | ||||
| } from 'crypto-js'; | ||||
| import { computed, ref } from 'vue'; | ||||
| import { convertHexToBin } from '../hash-text/hash-text.service'; | ||||
|  | ||||
| const algos = { | ||||
|   MD5: HmacMD5, | ||||
|   RIPEMD160: HmacRIPEMD160, | ||||
|   SHA1: HmacSHA1, | ||||
|   SHA3: HmacSHA3, | ||||
|   SHA224: HmacSHA224, | ||||
|   SHA256: HmacSHA256, | ||||
|   SHA384: HmacSHA384, | ||||
|   SHA512: HmacSHA512, | ||||
| } as const; | ||||
|  | ||||
| type Encoding = keyof typeof enc | 'Bin'; | ||||
|  | ||||
| function formatWithEncoding(words: lib.WordArray, encoding: Encoding) { | ||||
|   if (encoding === 'Bin') { | ||||
|     return convertHexToBin(words.toString(enc.Hex)); | ||||
|   } | ||||
|   return words.toString(enc[encoding]); | ||||
| } | ||||
|  | ||||
| const plainText = ref(''); | ||||
| const secret = ref(''); | ||||
| const hashFunction = ref<keyof typeof algos>('SHA256'); | ||||
| const encoding = ref<Encoding>('Hex'); | ||||
| const hmac = computed(() => | ||||
|   formatWithEncoding(algos[hashFunction.value](plainText.value, secret.value), encoding.value), | ||||
| ); | ||||
| const { copy } = useCopy({ source: hmac }); | ||||
| </script> | ||||
							
								
								
									
										12
									
								
								src/tools/hmac-generator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/hmac-generator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import { ShortTextRound } from '@vicons/material'; | ||||
| import { defineTool } from '../tool'; | ||||
|  | ||||
| export const tool = defineTool({ | ||||
|   name: 'Hmac generator', | ||||
|   path: '/hmac-generator', | ||||
|   description: | ||||
|     'Computes a hash-based message authentication code (HMAC) using a secret key and your favorite hashing function.', | ||||
|   keywords: ['hmac', 'generator', 'MD5', 'SHA1', 'SHA256', 'SHA224', 'SHA512', 'SHA384', 'SHA3', 'RIPEMD160'], | ||||
|   component: () => import('./hmac-generator.vue'), | ||||
|   icon: ShortTextRound, | ||||
| }); | ||||
							
								
								
									
										135
									
								
								src/tools/html-wysiwyg-editor/editor/editor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/tools/html-wysiwyg-editor/editor/editor.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| <template> | ||||
|   <n-card v-if="editor" class="editor"> | ||||
|     <template #header> | ||||
|       <menu-bar class="editor-header" :editor="editor" /> | ||||
|       <n-divider style="margin-top: 0" /> | ||||
|     </template> | ||||
|  | ||||
|     <editor-content class="editor-content" :editor="editor" /> | ||||
|   </n-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(.n-card-header) { | ||||
|   padding: 0; | ||||
| } | ||||
|  | ||||
| ::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> | ||||
							
								
								
									
										22
									
								
								src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| <template> | ||||
|   <n-tooltip trigger="hover"> | ||||
|     <template #trigger> | ||||
|       <n-button circle quaternary :type="isActive?.() ? 'primary' : 'default'" @click="action"> | ||||
|         <template #icon> | ||||
|           <n-icon :component="icon" /> | ||||
|         </template> | ||||
|       </n-button> | ||||
|     </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> | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user