mirror of
				https://github.com/CorentinTh/it-tools.git
				synced 2025-11-04 05:53:25 +00:00 
			
		
		
		
	Compare commits
	
		
			19 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					ebb7301a98 | ||
| 
						 | 
					def60e7248 | ||
| 
						 | 
					bf88836dbe | ||
| 
						 | 
					bfc2e24bbf | ||
| 
						 | 
					40872859a5 | ||
| 
						 | 
					cf723f144e | ||
| 
						 | 
					7f964941d3 | ||
| 
						 | 
					af075dcccc | ||
| 
						 | 
					274ff02b54 | ||
| 
						 | 
					679dd1c1f6 | ||
| 
						 | 
					4cd809bd0c | ||
| 
						 | 
					8d09086e78 | ||
| 
						 | 
					acf8bc11db | ||
| 
						 | 
					71e98e93e5 | ||
| 
						 | 
					1b5d4e72bd | ||
| 
						 | 
					8476cf319b | ||
| 
						 | 
					0ff853437b | ||
| 
						 | 
					39c8f92065 | ||
| 
						 | 
					35b5187119 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -26,3 +26,5 @@ coverage
 | 
				
			|||||||
*.njsproj
 | 
					*.njsproj
 | 
				
			||||||
*.sln
 | 
					*.sln
 | 
				
			||||||
*.sw?
 | 
					*.sw?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
							
								
								
									
										45
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -2,6 +2,51 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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.
 | 
					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.16.0](https://github.com/CorentinTh/it-tools/compare/v2.15.0...v2.16.0) (2022-12-21)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **search-bar:** use cmd + k to focus on mac ([bf88836](https://github.com/CorentinTh/it-tools/commit/bf88836dbe4037019e9545deaae1db06e5768cfb))
 | 
				
			||||||
 | 
					* **tool:** improved favorite tool management ([af075dc](https://github.com/CorentinTh/it-tools/commit/af075dccccec959a0863e6d11516206860bed91f))
 | 
				
			||||||
 | 
					* **tools:** added favorite tool handling ([4cd809b](https://github.com/CorentinTh/it-tools/commit/4cd809bd0c94836532f58a2ec6aa131694cce10d))
 | 
				
			||||||
 | 
					* **tracker:** added actions monitoring ([bfc2e24](https://github.com/CorentinTh/it-tools/commit/bfc2e24bbfc08f67ed9c9b1d93474029bc01dc8b))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Refactors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **clean:** removed empty style tag ([cf723f1](https://github.com/CorentinTh/it-tools/commit/cf723f144ee865b6de7323d3be58eb7a9586fa56))
 | 
				
			||||||
 | 
					* **clean:** removed unused import ([4087285](https://github.com/CorentinTh/it-tools/commit/40872859a580a20bb838b79db2b3c88c00995e37))
 | 
				
			||||||
 | 
					* **menu:** improve support button ([679dd1c](https://github.com/CorentinTh/it-tools/commit/679dd1c1f6265227cc9db60c55d83f8eaf8f72b4))
 | 
				
			||||||
 | 
					* **tracker:** better tracker injection ([def60e7](https://github.com/CorentinTh/it-tools/commit/def60e7248003e74ed67e9ff116b438bab410a92))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [2.15.0](https://github.com/CorentinTh/it-tools/compare/v2.14.1...v2.15.0) (2022-12-16)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **search-bar:** better search back result ([71e98e9](https://github.com/CorentinTh/it-tools/commit/71e98e93e5752cba934f67d679088524c4d3d2ad))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Bug Fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **integer-base-converter:** handle non-decimal char and better error message ([8476cf3](https://github.com/CorentinTh/it-tools/commit/8476cf319b7ebae87c7928592604a54833ac56ef))
 | 
				
			||||||
 | 
					* **tool-card:** correct text color on light mode for card description ([acf8bc1](https://github.com/CorentinTh/it-tools/commit/acf8bc11dbab85ab361edbe400ebbe5e52a11b89))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Refactors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **search-bar:** improved tool fuzzy search ([1b5d4e7](https://github.com/CorentinTh/it-tools/commit/1b5d4e72bdb222dd721a1e484c3e5d73bb62d2b1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### [2.14.1](https://github.com/CorentinTh/it-tools/compare/v2.14.0...v2.14.1) (2022-11-23)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [2.14.0](https://github.com/CorentinTh/it-tools/compare/v2.13.0...v2.14.0) (2022-11-23)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **new-tool:** chmod calculator ([35b5187](https://github.com/CorentinTh/it-tools/commit/35b518711938c2bc88f35d104bb35d9956f0c267))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## [2.13.0](https://github.com/CorentinTh/it-tools/compare/v2.11.0...v2.13.0) (2022-11-14)
 | 
					## [2.13.0](https://github.com/CorentinTh/it-tools/compare/v2.11.0...v2.13.0) (2022-11-14)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										48
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								README.md
									
									
									
									
									
								
							@@ -10,46 +10,53 @@ You have an idea of a tool? Submit a [feature request](https://github.com/Corent
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Contribute
 | 
					## Contribute
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Recommended IDE Setup
 | 
					## 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Customize configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					See [Vite Configuration Reference](https://vitejs.dev/config/).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Project Setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
nvm use
 | 
					pnpm install
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Project Setup
 | 
					### Compile and Hot-Reload for Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
npm install
 | 
					pnpm dev
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Compile and Hot-Reload for Development
 | 
					### Type-Check, Compile and Minify for Production
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
npm run dev
 | 
					pnpm build
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Type-Check, Compile and Minify for Production
 | 
					### Run Unit Tests with [Vitest](https://vitest.dev/)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
npm run build
 | 
					pnpm test
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Run Unit Tests with [Vitest](https://vitest.dev/)
 | 
					### Lint with [ESLint](https://eslint.org/)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
npm run test
 | 
					pnpm lint
 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Lint with [ESLint](https://eslint.org/)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```sh
 | 
					 | 
				
			||||||
npm run lint
 | 
					 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Create a new tool
 | 
					### Create a new tool
 | 
				
			||||||
@@ -68,12 +75,9 @@ Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr).
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
This project is continuously deployed using [vercel.com](https://vercel.com).
 | 
					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-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>
 | 
					<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
 | 
					## License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This project is under the [GNU GPLv3](LICENSE).
 | 
					This project is under the [GNU GPLv3](LICENSE).
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "it-tools",
 | 
					  "name": "it-tools",
 | 
				
			||||||
  "version": "2.13.0",
 | 
					  "version": "2.16.0",
 | 
				
			||||||
  "description": "Collection of handy online tools for developers, with great UX. ",
 | 
					  "description": "Collection of handy online tools for developers, with great UX. ",
 | 
				
			||||||
  "keywords": [
 | 
					  "keywords": [
 | 
				
			||||||
    "productivity",
 | 
					    "productivity",
 | 
				
			||||||
@@ -45,6 +45,7 @@
 | 
				
			|||||||
    "crypto-js": "^4.1.1",
 | 
					    "crypto-js": "^4.1.1",
 | 
				
			||||||
    "date-fns": "^2.29.3",
 | 
					    "date-fns": "^2.29.3",
 | 
				
			||||||
    "figue": "^1.2.0",
 | 
					    "figue": "^1.2.0",
 | 
				
			||||||
 | 
					    "fuse.js": "^6.6.2",
 | 
				
			||||||
    "highlight.js": "^11.6.0",
 | 
					    "highlight.js": "^11.6.0",
 | 
				
			||||||
    "json5": "^2.2.1",
 | 
					    "json5": "^2.2.1",
 | 
				
			||||||
    "lodash": "^4.17.21",
 | 
					    "lodash": "^4.17.21",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										7
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -38,6 +38,7 @@ specifiers:
 | 
				
			|||||||
  eslint-plugin-import: ^2.26.0
 | 
					  eslint-plugin-import: ^2.26.0
 | 
				
			||||||
  eslint-plugin-vue: ^8.7.1
 | 
					  eslint-plugin-vue: ^8.7.1
 | 
				
			||||||
  figue: ^1.2.0
 | 
					  figue: ^1.2.0
 | 
				
			||||||
 | 
					  fuse.js: ^6.6.2
 | 
				
			||||||
  highlight.js: ^11.6.0
 | 
					  highlight.js: ^11.6.0
 | 
				
			||||||
  jsdom: ^19.0.0
 | 
					  jsdom: ^19.0.0
 | 
				
			||||||
  json5: ^2.2.1
 | 
					  json5: ^2.2.1
 | 
				
			||||||
@@ -81,6 +82,7 @@ dependencies:
 | 
				
			|||||||
  crypto-js: 4.1.1
 | 
					  crypto-js: 4.1.1
 | 
				
			||||||
  date-fns: 2.29.3
 | 
					  date-fns: 2.29.3
 | 
				
			||||||
  figue: 1.2.0
 | 
					  figue: 1.2.0
 | 
				
			||||||
 | 
					  fuse.js: 6.6.2
 | 
				
			||||||
  highlight.js: 11.6.0
 | 
					  highlight.js: 11.6.0
 | 
				
			||||||
  json5: 2.2.1
 | 
					  json5: 2.2.1
 | 
				
			||||||
  lodash: 4.17.21
 | 
					  lodash: 4.17.21
 | 
				
			||||||
@@ -4054,6 +4056,11 @@ packages:
 | 
				
			|||||||
    resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
 | 
					    resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
 | 
				
			||||||
    dev: true
 | 
					    dev: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /fuse.js/6.6.2:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==}
 | 
				
			||||||
 | 
					    engines: {node: '>=10'}
 | 
				
			||||||
 | 
					    dev: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /gensync/1.0.0-beta.2:
 | 
					  /gensync/1.0.0-beta.2:
 | 
				
			||||||
    resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
 | 
					    resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
 | 
				
			||||||
    engines: {node: '>=6.9.0'}
 | 
					    engines: {node: '>=6.9.0'}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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>
 | 
				
			||||||
@@ -6,11 +6,11 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import type { ITool } from '@/tools/tool';
 | 
					import type { Tool } from '@/tools/tools.types';
 | 
				
			||||||
import { useThemeVars } from 'naive-ui';
 | 
					import { useThemeVars } from 'naive-ui';
 | 
				
			||||||
import { toRefs } from 'vue';
 | 
					import { toRefs } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{ tool: ITool }>();
 | 
					const props = defineProps<{ tool: Tool }>();
 | 
				
			||||||
const { tool } = toRefs(props);
 | 
					const { tool } = toRefs(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const theme = useThemeVars();
 | 
					const theme = useThemeVars();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,36 +1,38 @@
 | 
				
			|||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { useFuzzySearch } from '@/composable/fuzzySearch';
 | 
				
			||||||
 | 
					import { useTracker } from '@/modules/tracker/tracker.services';
 | 
				
			||||||
import { tools } from '@/tools';
 | 
					import { tools } from '@/tools';
 | 
				
			||||||
 | 
					import type { Tool } from '@/tools/tools.types';
 | 
				
			||||||
import { SearchRound } from '@vicons/material';
 | 
					import { SearchRound } from '@vicons/material';
 | 
				
			||||||
import { useMagicKeys, whenever } from '@vueuse/core';
 | 
					import { useMagicKeys, whenever } from '@vueuse/core';
 | 
				
			||||||
import { deburr } from 'lodash';
 | 
					import type { NInput } from 'naive-ui';
 | 
				
			||||||
import { computed, ref } from 'vue';
 | 
					import { computed, h, ref } from 'vue';
 | 
				
			||||||
import { useRouter } from 'vue-router';
 | 
					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 router = useRouter();
 | 
				
			||||||
 | 
					const { tracker } = useTracker();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const queryString = ref('');
 | 
					const queryString = ref('');
 | 
				
			||||||
 | 
					const inputEl = ref<HTMLElement>();
 | 
				
			||||||
const cleanString = (s: string) => deburr(s.trim().toLowerCase());
 | 
					const displayDropDown = ref(true);
 | 
				
			||||||
 | 
					const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
 | 
				
			||||||
const searchableTools = tools.map(({ name, description, keywords, path }) => ({
 | 
					 | 
				
			||||||
  searchableText: [name, description, ...keywords].map(cleanString).join(' '),
 | 
					 | 
				
			||||||
  path,
 | 
					 | 
				
			||||||
  name,
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const options = computed(() => {
 | 
					const options = computed(() => {
 | 
				
			||||||
  const query = cleanString(queryString.value);
 | 
					  if (queryString.value === '') {
 | 
				
			||||||
 | 
					    return tools.map(toolToOption);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return searchableTools
 | 
					  return searchResult.value.map(toolToOption);
 | 
				
			||||||
    .filter(({ searchableText }) => searchableText.includes(query))
 | 
					 | 
				
			||||||
    .map(({ name, path }) => ({ label: name, value: path }));
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function onSelect(path: string) {
 | 
					const { searchResult } = useFuzzySearch({
 | 
				
			||||||
  router.push(path);
 | 
					  search: queryString,
 | 
				
			||||||
  queryString.value = '';
 | 
					  data: tools,
 | 
				
			||||||
}
 | 
					  options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
const focusTarget = ref();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const keys = useMagicKeys({
 | 
					const keys = useMagicKeys({
 | 
				
			||||||
  passive: false,
 | 
					  passive: false,
 | 
				
			||||||
@@ -38,12 +40,40 @@ const keys = useMagicKeys({
 | 
				
			|||||||
    if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') {
 | 
					    if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (e.metaKey && e.key === 'k' && e.type === 'keydown') {
 | 
				
			||||||
 | 
					      e.preventDefault();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
whenever(keys.ctrl_k, () => {
 | 
					whenever(keys.ctrl_k, claimFocus);
 | 
				
			||||||
  focusTarget.value.focus();
 | 
					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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
@@ -51,16 +81,21 @@ whenever(keys.ctrl_k, () => {
 | 
				
			|||||||
    <n-auto-complete
 | 
					    <n-auto-complete
 | 
				
			||||||
      v-model:value="queryString"
 | 
					      v-model:value="queryString"
 | 
				
			||||||
      :options="options"
 | 
					      :options="options"
 | 
				
			||||||
      :input-props="{ autocomplete: 'disabled' }"
 | 
					      :on-select="(value) => onSelect(String(value))"
 | 
				
			||||||
      :on-select="onSelect"
 | 
					      :render-label="renderOption"
 | 
				
			||||||
 | 
					      :default-value="'aa'"
 | 
				
			||||||
 | 
					      :get-show="() => displayDropDown"
 | 
				
			||||||
 | 
					      :on-focus="onFocus"
 | 
				
			||||||
 | 
					      @update:value="() => (displayDropDown = true)"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
 | 
					      <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
 | 
				
			||||||
        <n-input
 | 
					        <n-input
 | 
				
			||||||
          ref="focusTarget"
 | 
					          ref="inputEl"
 | 
				
			||||||
          round
 | 
					          round
 | 
				
			||||||
          clearable
 | 
					          clearable
 | 
				
			||||||
          placeholder="Search a tool... [Ctrl + K]"
 | 
					          :placeholder="`Search a tool (use ${isMac ? 'Cmd' : 'Ctrl'} + K to focus)`"
 | 
				
			||||||
          :value="slotValue"
 | 
					          :value="slotValue"
 | 
				
			||||||
 | 
					          :input-props="{ autocomplete: 'disabled' }"
 | 
				
			||||||
          @input="handleInput"
 | 
					          @input="handleInput"
 | 
				
			||||||
          @focus="handleFocus"
 | 
					          @focus="handleFocus"
 | 
				
			||||||
          @blur="handleBlur"
 | 
					          @blur="handleBlur"
 | 
				
			||||||
@@ -73,9 +108,3 @@ whenever(keys.ctrl_k, () => {
 | 
				
			|||||||
    </n-auto-complete>
 | 
					    </n-auto-complete>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</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>
 | 
				
			||||||
@@ -3,17 +3,21 @@
 | 
				
			|||||||
    <n-card class="tool-card">
 | 
					    <n-card class="tool-card">
 | 
				
			||||||
      <n-space justify="space-between" align="center">
 | 
					      <n-space justify="space-between" align="center">
 | 
				
			||||||
        <n-icon class="icon" size="40" :component="tool.icon" />
 | 
					        <n-icon class="icon" size="40" :component="tool.icon" />
 | 
				
			||||||
        <n-tag
 | 
					        <n-space align="center">
 | 
				
			||||||
          v-if="tool.isNew"
 | 
					          <n-tag
 | 
				
			||||||
          size="small"
 | 
					            v-if="tool.isNew"
 | 
				
			||||||
          class="badge-new"
 | 
					            size="small"
 | 
				
			||||||
          round
 | 
					            class="badge-new"
 | 
				
			||||||
          type="success"
 | 
					            round
 | 
				
			||||||
          :bordered="false"
 | 
					            type="success"
 | 
				
			||||||
          :color="{ color: theme.primaryColor, textColor: theme.tagColor }"
 | 
					            :bordered="false"
 | 
				
			||||||
        >
 | 
					            :color="{ color: theme.primaryColor, textColor: theme.tagColor }"
 | 
				
			||||||
          New
 | 
					          >
 | 
				
			||||||
        </n-tag>
 | 
					            New
 | 
				
			||||||
 | 
					          </n-tag>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <favorite-button :tool="tool" />
 | 
				
			||||||
 | 
					        </n-space>
 | 
				
			||||||
      </n-space>
 | 
					      </n-space>
 | 
				
			||||||
      <n-h3 class="title">
 | 
					      <n-h3 class="title">
 | 
				
			||||||
        <n-ellipsis>{{ tool.name }}</n-ellipsis>
 | 
					        <n-ellipsis>{{ tool.name }}</n-ellipsis>
 | 
				
			||||||
@@ -29,11 +33,12 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import type { ITool } from '@/tools/tool';
 | 
					import type { Tool } from '@/tools/tools.types';
 | 
				
			||||||
import { useThemeVars } from 'naive-ui';
 | 
					import { useThemeVars } from 'naive-ui';
 | 
				
			||||||
import { toRefs } from 'vue';
 | 
					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 { tool } = toRefs(props);
 | 
				
			||||||
const theme = useThemeVars();
 | 
					const theme = useThemeVars();
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
@@ -50,7 +55,7 @@ a {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  .icon {
 | 
					  .icon {
 | 
				
			||||||
    opacity: 0.6;
 | 
					    opacity: 0.6;
 | 
				
			||||||
    color: #ffffff;
 | 
					    color: v-bind('theme.textColorBase');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .title {
 | 
					  .title {
 | 
				
			||||||
@@ -59,7 +64,7 @@ a {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  .description {
 | 
					  .description {
 | 
				
			||||||
    opacity: 0.6;
 | 
					    opacity: 0.6;
 | 
				
			||||||
    color: #ffffff;
 | 
					    color: v-bind('theme.textColorBase');
 | 
				
			||||||
    margin: 5px 0;
 | 
					    margin: 5px 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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 };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,13 +1,15 @@
 | 
				
			|||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { NIcon, useThemeVars, type MenuGroupOption } from 'naive-ui';
 | 
					import { NIcon, useThemeVars, type MenuGroupOption } from 'naive-ui';
 | 
				
			||||||
import { h } from 'vue';
 | 
					import { computed, h } from 'vue';
 | 
				
			||||||
import { RouterLink, useRoute } from 'vue-router';
 | 
					import { RouterLink, useRoute } from 'vue-router';
 | 
				
			||||||
import { Heart, Menu2, Home2 } from '@vicons/tabler';
 | 
					import { Heart, Menu2, Home2 } from '@vicons/tabler';
 | 
				
			||||||
import { toolsByCategory } from '@/tools';
 | 
					import { toolsByCategory } from '@/tools';
 | 
				
			||||||
import { useStyleStore } from '@/stores/style.store';
 | 
					import { useStyleStore } from '@/stores/style.store';
 | 
				
			||||||
import { config } from '@/config';
 | 
					import { config } from '@/config';
 | 
				
			||||||
import MenuIconItem from '@/components/MenuIconItem.vue';
 | 
					import MenuIconItem from '@/components/MenuIconItem.vue';
 | 
				
			||||||
import type { ITool } from '@/tools/tool';
 | 
					import type { Tool } from '@/tools/tools.types';
 | 
				
			||||||
 | 
					import { useToolStore } from '@/tools/tools.store';
 | 
				
			||||||
 | 
					import { useTracker } from '@/modules/tracker/tracker.services';
 | 
				
			||||||
import SearchBar from '../components/SearchBar.vue';
 | 
					import SearchBar from '../components/SearchBar.vue';
 | 
				
			||||||
import HeroGradient from '../assets/hero-gradient.svg?component';
 | 
					import HeroGradient from '../assets/hero-gradient.svg?component';
 | 
				
			||||||
import MenuLayout from '../components/MenuLayout.vue';
 | 
					import MenuLayout from '../components/MenuLayout.vue';
 | 
				
			||||||
@@ -19,19 +21,30 @@ const styleStore = useStyleStore();
 | 
				
			|||||||
const version = config.app.version;
 | 
					const version = config.app.version;
 | 
				
			||||||
const commitSha = config.app.lastCommitSha.slice(0, 7);
 | 
					const commitSha = config.app.lastCommitSha.slice(0, 7);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const makeLabel = (tool: ITool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name });
 | 
					const makeLabel = (tool: Tool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name });
 | 
				
			||||||
const makeIcon = (tool: ITool) => () => h(MenuIconItem, { tool });
 | 
					const makeIcon = (tool: Tool) => () => h(MenuIconItem, { tool });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
 | 
					const { tracker } = useTracker();
 | 
				
			||||||
  label: category.name,
 | 
					
 | 
				
			||||||
  key: category.name,
 | 
					const toolStore = useToolStore();
 | 
				
			||||||
  type: 'group',
 | 
					
 | 
				
			||||||
  children: category.components.map((tool) => ({
 | 
					const menuOptions = computed<MenuGroupOption[]>(() =>
 | 
				
			||||||
    label: makeLabel(tool),
 | 
					  [
 | 
				
			||||||
    icon: makeIcon(tool),
 | 
					    ...(toolStore.favoriteTools.length > 0
 | 
				
			||||||
    key: tool.name,
 | 
					      ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }]
 | 
				
			||||||
 | 
					      : []),
 | 
				
			||||||
 | 
					    ...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,
 | 
				
			||||||
 | 
					    })),
 | 
				
			||||||
  })),
 | 
					  })),
 | 
				
			||||||
}));
 | 
					);
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
@@ -145,6 +158,9 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
 | 
				
			|||||||
              href="https://github.com/sponsors/CorentinTh"
 | 
					              href="https://github.com/sponsors/CorentinTh"
 | 
				
			||||||
              rel="noopener"
 | 
					              rel="noopener"
 | 
				
			||||||
              target="_blank"
 | 
					              target="_blank"
 | 
				
			||||||
 | 
					              class="support-button"
 | 
				
			||||||
 | 
					              :bordered="false"
 | 
				
			||||||
 | 
					              @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              Buy me a coffee
 | 
					              Buy me a coffee
 | 
				
			||||||
              <n-icon v-if="!styleStore.isSmallScreen" :component="Heart" style="margin-left: 5px" />
 | 
					              <n-icon v-if="!styleStore.isSmallScreen" :component="Heart" style="margin-left: 5px" />
 | 
				
			||||||
@@ -170,6 +186,19 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
 | 
				
			|||||||
//     background-size: @size @size;
 | 
					//     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 {
 | 
					.footer {
 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
  color: #838587;
 | 
					  color: #838587;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,22 +3,22 @@ import { useRoute } from 'vue-router';
 | 
				
			|||||||
import { useHead } from '@vueuse/head';
 | 
					import { useHead } from '@vueuse/head';
 | 
				
			||||||
import type { HeadObject } from '@vueuse/head';
 | 
					import type { HeadObject } from '@vueuse/head';
 | 
				
			||||||
import { computed } from 'vue';
 | 
					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';
 | 
					import BaseLayout from './base.layout.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const route = useRoute();
 | 
					const route = useRoute();
 | 
				
			||||||
const theme = useThemeVars();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const head = computed<HeadObject>(() => ({
 | 
					const head = computed<HeadObject>(() => ({
 | 
				
			||||||
  title: `${route.meta.name} - IT Tools`,
 | 
					  title: `${route.meta.name} - IT Tools`,
 | 
				
			||||||
  meta: [
 | 
					  meta: [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      name: 'description',
 | 
					      name: 'description',
 | 
				
			||||||
      content: route.meta.description,
 | 
					      content: route.meta?.description as string,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      name: 'keywords',
 | 
					      name: 'keywords',
 | 
				
			||||||
      content: route.meta.keywords,
 | 
					      content: ((route.meta.keywords ?? []) as string[]).join(','),
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
@@ -29,22 +29,18 @@ useHead(head);
 | 
				
			|||||||
  <base-layout>
 | 
					  <base-layout>
 | 
				
			||||||
    <div class="tool-layout">
 | 
					    <div class="tool-layout">
 | 
				
			||||||
      <div class="tool-header">
 | 
					      <div class="tool-header">
 | 
				
			||||||
        <n-h1>
 | 
					        <n-space align="center" justify="space-between" :wrap="false">
 | 
				
			||||||
          {{ route.meta.name }}
 | 
					          <n-h1>
 | 
				
			||||||
 | 
					            {{ route.meta.name }}
 | 
				
			||||||
 | 
					          </n-h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <n-tag
 | 
					          <div>
 | 
				
			||||||
            v-if="route.meta.isNew"
 | 
					            <favorite-button :tool="{name: route.meta.name} as Tool" />
 | 
				
			||||||
            round
 | 
					          </div>
 | 
				
			||||||
            type="success"
 | 
					        </n-space>
 | 
				
			||||||
            :bordered="false"
 | 
					 | 
				
			||||||
            :color="{ color: theme.primaryColor, textColor: theme.tagColor }"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            New tool
 | 
					 | 
				
			||||||
          </n-tag>
 | 
					 | 
				
			||||||
          <!-- <span class="new-tool-badge">New !</span> -->
 | 
					 | 
				
			||||||
        </n-h1>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="separator" />
 | 
					        <div class="separator" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="description">
 | 
					        <div class="description">
 | 
				
			||||||
          {{ route.meta.description }}
 | 
					          {{ route.meta.description }}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@@ -92,6 +88,7 @@ useHead(head);
 | 
				
			|||||||
      width: 200px;
 | 
					      width: 200px;
 | 
				
			||||||
      height: 2px;
 | 
					      height: 2px;
 | 
				
			||||||
      background: rgb(161, 161, 161);
 | 
					      background: rgb(161, 161, 161);
 | 
				
			||||||
 | 
					      opacity: 0.2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      margin: 10px 0;
 | 
					      margin: 10px 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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,7 +1,9 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useTracker } from '@/modules/tracker/tracker.services';
 | 
				
			||||||
import { useHead } from '@vueuse/head';
 | 
					import { useHead } from '@vueuse/head';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
useHead({ title: 'About - IT Tools' });
 | 
					useHead({ title: 'About - IT Tools' });
 | 
				
			||||||
 | 
					const { tracker } = useTracker();
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
@@ -25,6 +27,7 @@ useHead({ title: 'About - IT Tools' });
 | 
				
			|||||||
        href="https://github.com/sponsors/CorentinTh"
 | 
					        href="https://github.com/sponsors/CorentinTh"
 | 
				
			||||||
        rel="noopener"
 | 
					        rel="noopener"
 | 
				
			||||||
        target="_blank"
 | 
					        target="_blank"
 | 
				
			||||||
 | 
					        @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        sponsoring me </n-button
 | 
					        sponsoring me </n-button
 | 
				
			||||||
      >.
 | 
					      >.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,41 +1,71 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { toolsWithCategory } from '@/tools';
 | 
					import { useToolStore } from '@/tools/tools.store';
 | 
				
			||||||
import { Heart } from '@vicons/tabler';
 | 
					import { Heart } from '@vicons/tabler';
 | 
				
			||||||
import { useHead } from '@vueuse/head';
 | 
					import { useHead } from '@vueuse/head';
 | 
				
			||||||
import ColoredCard from '../components/ColoredCard.vue';
 | 
					import ColoredCard from '../components/ColoredCard.vue';
 | 
				
			||||||
import ToolCard from '../components/ToolCard.vue';
 | 
					import ToolCard from '../components/ToolCard.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const toolStore = useToolStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
useHead({ title: 'IT Tools - Handy online tools for developers' });
 | 
					useHead({ title: 'IT Tools - Handy online tools for developers' });
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="home-page">
 | 
					  <div class="home-page">
 | 
				
			||||||
    <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
 | 
					    <div class="grid-wrapper">
 | 
				
			||||||
      <n-gi>
 | 
					      <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
 | 
				
			||||||
        <colored-card title="You like it-tools?" :icon="Heart">
 | 
					        <n-gi>
 | 
				
			||||||
          Give us a star on
 | 
					          <colored-card title="You like it-tools?" :icon="Heart">
 | 
				
			||||||
          <a
 | 
					            Give us a star on
 | 
				
			||||||
            href="https://github.com/CorentinTh/it-tools"
 | 
					            <a
 | 
				
			||||||
            rel="noopener"
 | 
					              href="https://github.com/CorentinTh/it-tools"
 | 
				
			||||||
            target="_blank"
 | 
					              rel="noopener"
 | 
				
			||||||
            aria-label="IT-Tools' github repository"
 | 
					              target="_blank"
 | 
				
			||||||
            >github</a
 | 
					              aria-label="IT-Tools' github repository"
 | 
				
			||||||
          >
 | 
					              >github</a
 | 
				
			||||||
          or follow us on
 | 
					            >
 | 
				
			||||||
          <a
 | 
					            or follow us on
 | 
				
			||||||
            href="https://twitter.com/ittoolsdottech"
 | 
					            <a
 | 
				
			||||||
            rel="noopener"
 | 
					              href="https://twitter.com/ittoolsdottech"
 | 
				
			||||||
            target="_blank"
 | 
					              rel="noopener"
 | 
				
			||||||
            aria-label="IT-Tools' twitter account"
 | 
					              target="_blank"
 | 
				
			||||||
            >twitter</a
 | 
					              aria-label="IT-Tools' twitter account"
 | 
				
			||||||
          >! Thank you
 | 
					              >twitter</a
 | 
				
			||||||
          <n-icon :component="Heart" />
 | 
					            >! Thank you
 | 
				
			||||||
        </colored-card>
 | 
					            <n-icon :component="Heart" />
 | 
				
			||||||
      </n-gi>
 | 
					          </colored-card>
 | 
				
			||||||
      <n-gi v-for="tool in toolsWithCategory" :key="tool.name">
 | 
					        </n-gi>
 | 
				
			||||||
        <tool-card :tool="tool" />
 | 
					      </n-grid>
 | 
				
			||||||
      </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>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -43,4 +73,27 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
 | 
				
			|||||||
.home-page {
 | 
					.home-page {
 | 
				
			||||||
  padding-top: 50px;
 | 
					  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>
 | 
					</style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,9 +53,11 @@ import {
 | 
				
			|||||||
  NTooltip,
 | 
					  NTooltip,
 | 
				
			||||||
  NUpload,
 | 
					  NUpload,
 | 
				
			||||||
  NUploadDragger,
 | 
					  NUploadDragger,
 | 
				
			||||||
 | 
					  NCheckbox,
 | 
				
			||||||
} from 'naive-ui';
 | 
					} from 'naive-ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const components = [
 | 
					const components = [
 | 
				
			||||||
 | 
					  NCheckbox,
 | 
				
			||||||
  NDynamicInput,
 | 
					  NDynamicInput,
 | 
				
			||||||
  NDatePicker,
 | 
					  NDatePicker,
 | 
				
			||||||
  NCode,
 | 
					  NCode,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import { config } from '@/config';
 | 
					import { config } from '@/config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Plausible from 'plausible-tracker';
 | 
					import Plausible from 'plausible-tracker';
 | 
				
			||||||
import type { App } from 'vue';
 | 
					import type { App } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -7,6 +8,6 @@ export const plausible = {
 | 
				
			|||||||
    const plausible = Plausible(config.plausible);
 | 
					    const plausible = Plausible(config.plausible);
 | 
				
			||||||
    plausible.enableAutoPageviews();
 | 
					    plausible.enableAutoPageviews();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    app.config.globalProperties.$plausible = plausible;
 | 
					    app.provide('plausible', plausible);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,12 @@ export const lightThemeOverrides: GlobalThemeOverrides = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Layout: { color: '#f1f5f9' },
 | 
					  Layout: { color: '#f1f5f9' },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  AutoComplete: {
 | 
				
			||||||
 | 
					    peers: {
 | 
				
			||||||
 | 
					      InternalSelectMenu: { height: '500px' },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const darkThemeOverrides: GlobalThemeOverrides = {
 | 
					export const darkThemeOverrides: GlobalThemeOverrides = {
 | 
				
			||||||
@@ -16,6 +22,12 @@ export const darkThemeOverrides: GlobalThemeOverrides = {
 | 
				
			|||||||
    primaryColorSuppl: '#36AD6AFF',
 | 
					    primaryColorSuppl: '#36AD6AFF',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  AutoComplete: {
 | 
				
			||||||
 | 
					    peers: {
 | 
				
			||||||
 | 
					      InternalSelectMenu: { height: '500px', color: '#1e1e1e' },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Menu: {
 | 
					  Menu: {
 | 
				
			||||||
    itemHeight: '32px',
 | 
					    itemHeight: '32px',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,5 +53,3 @@ const b64Validation = useValidation({
 | 
				
			|||||||
  rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }],
 | 
					  rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }],
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="less" scoped></style>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										68
									
								
								src/tools/chmod-calculator/chmod-calculator.service.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/tools/chmod-calculator/chmod-calculator.service.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					import { expect, describe, it } from 'vitest';
 | 
				
			||||||
 | 
					import { computeChmodOctalRepresentation } from './chmod-calculator.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('chmod-calculator', () => {
 | 
				
			||||||
 | 
					  describe('computeChmodOctalRepresentation', () => {
 | 
				
			||||||
 | 
					    it('get the octal representation from permissions', () => {
 | 
				
			||||||
 | 
					      expect(
 | 
				
			||||||
 | 
					        computeChmodOctalRepresentation({
 | 
				
			||||||
 | 
					          permissions: {
 | 
				
			||||||
 | 
					            owner: { read: true, write: true, execute: true },
 | 
				
			||||||
 | 
					            group: { read: true, write: true, execute: true },
 | 
				
			||||||
 | 
					            public: { read: true, write: true, execute: true },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ).to.eql('777');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(
 | 
				
			||||||
 | 
					        computeChmodOctalRepresentation({
 | 
				
			||||||
 | 
					          permissions: {
 | 
				
			||||||
 | 
					            owner: { read: false, write: false, execute: false },
 | 
				
			||||||
 | 
					            group: { read: false, write: false, execute: false },
 | 
				
			||||||
 | 
					            public: { read: false, write: false, execute: false },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ).to.eql('000');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(
 | 
				
			||||||
 | 
					        computeChmodOctalRepresentation({
 | 
				
			||||||
 | 
					          permissions: {
 | 
				
			||||||
 | 
					            owner: { read: false, write: true, execute: false },
 | 
				
			||||||
 | 
					            group: { read: false, write: true, execute: true },
 | 
				
			||||||
 | 
					            public: { read: true, write: false, execute: true },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ).to.eql('235');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(
 | 
				
			||||||
 | 
					        computeChmodOctalRepresentation({
 | 
				
			||||||
 | 
					          permissions: {
 | 
				
			||||||
 | 
					            owner: { read: true, write: false, execute: false },
 | 
				
			||||||
 | 
					            group: { read: false, write: true, execute: false },
 | 
				
			||||||
 | 
					            public: { read: false, write: false, execute: true },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ).to.eql('421');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(
 | 
				
			||||||
 | 
					        computeChmodOctalRepresentation({
 | 
				
			||||||
 | 
					          permissions: {
 | 
				
			||||||
 | 
					            owner: { read: false, write: false, execute: true },
 | 
				
			||||||
 | 
					            group: { read: false, write: true, execute: false },
 | 
				
			||||||
 | 
					            public: { read: true, write: false, execute: false },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ).to.eql('124');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(
 | 
				
			||||||
 | 
					        computeChmodOctalRepresentation({
 | 
				
			||||||
 | 
					          permissions: {
 | 
				
			||||||
 | 
					            owner: { read: false, write: true, execute: false },
 | 
				
			||||||
 | 
					            group: { read: false, write: true, execute: false },
 | 
				
			||||||
 | 
					            public: { read: false, write: true, execute: false },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ).to.eql('222');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										17
									
								
								src/tools/chmod-calculator/chmod-calculator.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/tools/chmod-calculator/chmod-calculator.service.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import _ from 'lodash';
 | 
				
			||||||
 | 
					import type { GroupPermissions, Permissions } from './chmod-calculator.types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { computeChmodOctalRepresentation };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string {
 | 
				
			||||||
 | 
					  const permissionValue = { read: 4, write: 2, execute: 1 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getGroupPermissionValue = (permission: GroupPermissions) =>
 | 
				
			||||||
 | 
					    _.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, 0) : 0), 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return [
 | 
				
			||||||
 | 
					    getGroupPermissionValue(permissions.owner),
 | 
				
			||||||
 | 
					    getGroupPermissionValue(permissions.group),
 | 
				
			||||||
 | 
					    getGroupPermissionValue(permissions.public),
 | 
				
			||||||
 | 
					  ].join('');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								src/tools/chmod-calculator/chmod-calculator.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/tools/chmod-calculator/chmod-calculator.types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					export type Scope = 'read' | 'write' | 'execute';
 | 
				
			||||||
 | 
					export type Group = 'owner' | 'group' | 'public';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type GroupPermissions = {
 | 
				
			||||||
 | 
					  [k in Scope]: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Permissions = {
 | 
				
			||||||
 | 
					  [k in Group]: GroupPermissions;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										83
									
								
								src/tools/chmod-calculator/chmod-calculator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/tools/chmod-calculator/chmod-calculator.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
 | 
				
			||||||
 | 
					      <thead>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					          <th class="text-center" scope="col"></th>
 | 
				
			||||||
 | 
					          <th class="text-center" scope="col">Owner (u)</th>
 | 
				
			||||||
 | 
					          <th class="text-center" scope="col">Group (g)</th>
 | 
				
			||||||
 | 
					          <th class="text-center" scope="col">Public (o)</th>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					      </thead>
 | 
				
			||||||
 | 
					      <tbody>
 | 
				
			||||||
 | 
					        <tr v-for="{ scope, title } of scopes" :key="scope">
 | 
				
			||||||
 | 
					          <td class="line-header">{{ title }}</td>
 | 
				
			||||||
 | 
					          <td v-for="group of groups" :key="group" class="text-center">
 | 
				
			||||||
 | 
					            <!-- <n-switch v-model:value="permissions[group][scope]" /> -->
 | 
				
			||||||
 | 
					            <n-checkbox v-model:checked="permissions[group][scope]" size="large" />
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					      </tbody>
 | 
				
			||||||
 | 
					    </n-table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="octal-result">
 | 
				
			||||||
 | 
					      {{ octal }}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <input-copyable :value="`chmod ${octal} path`" readonly style="margin-bottom: 5px" />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useThemeVars } from 'naive-ui';
 | 
				
			||||||
 | 
					import { computed, ref } from 'vue';
 | 
				
			||||||
 | 
					import { computeChmodOctalRepresentation } from './chmod-calculator.service';
 | 
				
			||||||
 | 
					import InputCopyable from '../../components/InputCopyable.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { Group, Scope } from './chmod-calculator.types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const themeVars = useThemeVars();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const scopes: { scope: Scope; title: string }[] = [
 | 
				
			||||||
 | 
					  { scope: 'read', title: 'Read (4)' },
 | 
				
			||||||
 | 
					  { scope: 'write', title: 'Write (2)' },
 | 
				
			||||||
 | 
					  { scope: 'execute', title: 'Execute (1)' },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					const groups: Group[] = ['owner', 'group', 'public'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const permissions = ref({
 | 
				
			||||||
 | 
					  owner: { read: false, write: false, execute: false },
 | 
				
			||||||
 | 
					  group: { read: false, write: false, execute: false },
 | 
				
			||||||
 | 
					  public: { read: false, write: false, execute: false },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="less" scoped>
 | 
				
			||||||
 | 
					.octal-result {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  font-size: 50px;
 | 
				
			||||||
 | 
					  font-family: monospace;
 | 
				
			||||||
 | 
					  color: v-bind('themeVars.primaryColor');
 | 
				
			||||||
 | 
					  margin: 20px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.permission-table {
 | 
				
			||||||
 | 
					  td,
 | 
				
			||||||
 | 
					  th {
 | 
				
			||||||
 | 
					    padding: 15px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @media screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					      padding: 5px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.line-header {
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  text-align: right;
 | 
				
			||||||
 | 
					  max-width: 80px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.text-center {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/tools/chmod-calculator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/tools/chmod-calculator/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import { FileInvoice } from '@vicons/tabler';
 | 
				
			||||||
 | 
					import { defineTool } from '../tool';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const tool = defineTool({
 | 
				
			||||||
 | 
					  name: 'Chmod calculator',
 | 
				
			||||||
 | 
					  path: '/chmod-calculator',
 | 
				
			||||||
 | 
					  description: 'Compute your chmod permissions and commands with this online chmod calculator.',
 | 
				
			||||||
 | 
					  keywords: [
 | 
				
			||||||
 | 
					    'chmod',
 | 
				
			||||||
 | 
					    'calculator',
 | 
				
			||||||
 | 
					    'file',
 | 
				
			||||||
 | 
					    'permission',
 | 
				
			||||||
 | 
					    'files',
 | 
				
			||||||
 | 
					    'directory',
 | 
				
			||||||
 | 
					    'folder',
 | 
				
			||||||
 | 
					    'recursive',
 | 
				
			||||||
 | 
					    'generator',
 | 
				
			||||||
 | 
					    'octal',
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  component: () => import('./chmod-calculator.vue'),
 | 
				
			||||||
 | 
					  icon: FileInvoice,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -94,5 +94,3 @@ const hmac = computed(() =>
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
const { copy } = useCopy({ source: hmac });
 | 
					const { copy } = useCopy({ source: hmac });
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="less" scoped></style>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,4 @@
 | 
				
			|||||||
import { LockOpen } from '@vicons/tabler';
 | 
					import { tool as chmodCalculator } from './chmod-calculator';
 | 
				
			||||||
import type { ToolCategory } from './tool';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { tool as mimeTypes } from './mime-types';
 | 
					import { tool as mimeTypes } from './mime-types';
 | 
				
			||||||
import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
 | 
					import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
 | 
				
			||||||
import { tool as base64FileConverter } from './base64-file-converter';
 | 
					import { tool as base64FileConverter } from './base64-file-converter';
 | 
				
			||||||
@@ -35,16 +33,15 @@ import { tool as tokenGenerator } from './token-generator';
 | 
				
			|||||||
import { tool as urlEncoder } from './url-encoder';
 | 
					import { tool as urlEncoder } from './url-encoder';
 | 
				
			||||||
import { tool as urlParser } from './url-parser';
 | 
					import { tool as urlParser } from './url-parser';
 | 
				
			||||||
import { tool as uuidGenerator } from './uuid-generator';
 | 
					import { tool as uuidGenerator } from './uuid-generator';
 | 
				
			||||||
 | 
					import type { ToolCategory } from './tools.types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const toolsByCategory: ToolCategory[] = [
 | 
					export const toolsByCategory: ToolCategory[] = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: 'Crypto',
 | 
					    name: 'Crypto',
 | 
				
			||||||
    icon: LockOpen,
 | 
					 | 
				
			||||||
    components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator],
 | 
					    components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: 'Converter',
 | 
					    name: 'Converter',
 | 
				
			||||||
    icon: LockOpen,
 | 
					 | 
				
			||||||
    components: [
 | 
					    components: [
 | 
				
			||||||
      dateTimeConverter,
 | 
					      dateTimeConverter,
 | 
				
			||||||
      baseConverter,
 | 
					      baseConverter,
 | 
				
			||||||
@@ -57,7 +54,6 @@ export const toolsByCategory: ToolCategory[] = [
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: 'Web',
 | 
					    name: 'Web',
 | 
				
			||||||
    icon: LockOpen,
 | 
					 | 
				
			||||||
    components: [
 | 
					    components: [
 | 
				
			||||||
      urlEncoder,
 | 
					      urlEncoder,
 | 
				
			||||||
      htmlEntities,
 | 
					      htmlEntities,
 | 
				
			||||||
@@ -71,27 +67,22 @@ export const toolsByCategory: ToolCategory[] = [
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: 'Images',
 | 
					    name: 'Images',
 | 
				
			||||||
    icon: LockOpen,
 | 
					 | 
				
			||||||
    components: [qrCodeGenerator, svgPlaceholderGenerator],
 | 
					    components: [qrCodeGenerator, svgPlaceholderGenerator],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: 'Development',
 | 
					    name: 'Development',
 | 
				
			||||||
    icon: LockOpen,
 | 
					    components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify, chmodCalculator],
 | 
				
			||||||
    components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify],
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: 'Math',
 | 
					    name: 'Math',
 | 
				
			||||||
    icon: LockOpen,
 | 
					 | 
				
			||||||
    components: [mathEvaluator, etaCalculator],
 | 
					    components: [mathEvaluator, etaCalculator],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: 'Measurement',
 | 
					    name: 'Measurement',
 | 
				
			||||||
    icon: LockOpen,
 | 
					 | 
				
			||||||
    components: [chronometer],
 | 
					    components: [chronometer],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: 'Text',
 | 
					    name: 'Text',
 | 
				
			||||||
    icon: LockOpen,
 | 
					 | 
				
			||||||
    components: [loremIpsumGenerator, textStatistics],
 | 
					    components: [loremIpsumGenerator, textStatistics],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ export function convertBase({ value, fromBase, toBase }: { value: string; fromBa
 | 
				
			|||||||
    .reverse()
 | 
					    .reverse()
 | 
				
			||||||
    .reduce((carry: number, digit: string, index: number) => {
 | 
					    .reduce((carry: number, digit: string, index: number) => {
 | 
				
			||||||
      if (!fromRange.includes(digit)) {
 | 
					      if (!fromRange.includes(digit)) {
 | 
				
			||||||
        throw new Error('Invalid digit `' + digit + '` for base ' + fromBase + '.');
 | 
					        throw new Error('Invalid digit "' + digit + '" for base ' + fromBase + '.');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return (carry += fromRange.indexOf(digit) * Math.pow(fromBase, index));
 | 
					      return (carry += fromRange.indexOf(digit) * Math.pow(fromBase, index));
 | 
				
			||||||
    }, 0);
 | 
					    }, 0);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@
 | 
				
			|||||||
      <div v-if="styleStore.isSmallScreen">
 | 
					      <div v-if="styleStore.isSmallScreen">
 | 
				
			||||||
        <n-input-group>
 | 
					        <n-input-group>
 | 
				
			||||||
          <n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
 | 
					          <n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
 | 
				
			||||||
          <n-input-number v-model:value="inputNumber" min="0" style="width: 100%" />
 | 
					          <n-input v-model:value="input" style="width: 100%" :status="error ? 'error' : undefined" />
 | 
				
			||||||
        </n-input-group>
 | 
					        </n-input-group>
 | 
				
			||||||
        <n-input-group>
 | 
					        <n-input-group>
 | 
				
			||||||
          <n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label>
 | 
					          <n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label>
 | 
				
			||||||
@@ -14,51 +14,65 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      <n-input-group v-else>
 | 
					      <n-input-group v-else>
 | 
				
			||||||
        <n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
 | 
					        <n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
 | 
				
			||||||
        <n-input-number v-model:value="inputNumber" min="0" />
 | 
					        <n-input v-model:value="input" :status="error ? 'error' : undefined" />
 | 
				
			||||||
        <n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label>
 | 
					        <n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label>
 | 
				
			||||||
        <n-input-number v-model:value="inputBase" max="64" min="2" />
 | 
					        <n-input-number v-model:value="inputBase" max="64" min="2" />
 | 
				
			||||||
      </n-input-group>
 | 
					      </n-input-group>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert>
 | 
				
			||||||
      <n-divider />
 | 
					      <n-divider />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <n-input-group>
 | 
					      <n-input-group>
 | 
				
			||||||
        <n-input-group-label style="flex: 0 0 170px"> Binary (2): </n-input-group-label>
 | 
					        <n-input-group-label style="flex: 0 0 170px"> Binary (2): </n-input-group-label>
 | 
				
			||||||
        <input-copyable :value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 2 })" readonly />
 | 
					        <input-copyable
 | 
				
			||||||
 | 
					          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
 | 
				
			||||||
 | 
					          readonly
 | 
				
			||||||
 | 
					          placeholder="Binary version will be here..."
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
      </n-input-group>
 | 
					      </n-input-group>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <n-input-group>
 | 
					      <n-input-group>
 | 
				
			||||||
        <n-input-group-label style="flex: 0 0 170px"> Octal (8): </n-input-group-label>
 | 
					        <n-input-group-label style="flex: 0 0 170px"> Octal (8): </n-input-group-label>
 | 
				
			||||||
        <input-copyable :value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 8 })" readonly />
 | 
					        <input-copyable
 | 
				
			||||||
 | 
					          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
 | 
				
			||||||
 | 
					          readonly
 | 
				
			||||||
 | 
					          placeholder="Octal version will be here..."
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
      </n-input-group>
 | 
					      </n-input-group>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <n-input-group>
 | 
					      <n-input-group>
 | 
				
			||||||
        <n-input-group-label style="flex: 0 0 170px"> Decimal (10): </n-input-group-label>
 | 
					        <n-input-group-label style="flex: 0 0 170px"> Decimal (10): </n-input-group-label>
 | 
				
			||||||
        <input-copyable
 | 
					        <input-copyable
 | 
				
			||||||
          :value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 10 })"
 | 
					          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
 | 
				
			||||||
          readonly
 | 
					          readonly
 | 
				
			||||||
 | 
					          placeholder="Decimal version will be here..."
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </n-input-group>
 | 
					      </n-input-group>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <n-input-group>
 | 
					      <n-input-group>
 | 
				
			||||||
        <n-input-group-label style="flex: 0 0 170px"> Hexadecimal (16): </n-input-group-label>
 | 
					        <n-input-group-label style="flex: 0 0 170px"> Hexadecimal (16): </n-input-group-label>
 | 
				
			||||||
        <input-copyable
 | 
					        <input-copyable
 | 
				
			||||||
          :value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 16 })"
 | 
					          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })"
 | 
				
			||||||
          readonly
 | 
					          readonly
 | 
				
			||||||
 | 
					          placeholder="Decimal version will be here..."
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </n-input-group>
 | 
					      </n-input-group>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <n-input-group>
 | 
					      <n-input-group>
 | 
				
			||||||
        <n-input-group-label style="flex: 0 0 170px"> Base64 (64): </n-input-group-label>
 | 
					        <n-input-group-label style="flex: 0 0 170px"> Base64 (64): </n-input-group-label>
 | 
				
			||||||
        <input-copyable
 | 
					        <input-copyable
 | 
				
			||||||
          :value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 64 })"
 | 
					          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })"
 | 
				
			||||||
          readonly
 | 
					          readonly
 | 
				
			||||||
 | 
					          placeholder="Base64 version will be here..."
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </n-input-group>
 | 
					      </n-input-group>
 | 
				
			||||||
      <n-input-group>
 | 
					      <n-input-group>
 | 
				
			||||||
        <n-input-group-label style="flex: 0 0 85px"> Custom: </n-input-group-label>
 | 
					        <n-input-group-label style="flex: 0 0 85px"> Custom: </n-input-group-label>
 | 
				
			||||||
        <n-input-number v-model:value="outputBase" style="flex: 0 0 86px" max="64" min="2" />
 | 
					        <n-input-number v-model:value="outputBase" style="flex: 0 0 86px" max="64" min="2" />
 | 
				
			||||||
        <input-copyable
 | 
					        <input-copyable
 | 
				
			||||||
          :value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: outputBase })"
 | 
					          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })"
 | 
				
			||||||
          readonly
 | 
					          readonly
 | 
				
			||||||
 | 
					          :placeholder="`Base ${outputBase} will be here...`"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </n-input-group>
 | 
					      </n-input-group>
 | 
				
			||||||
    </n-card>
 | 
					    </n-card>
 | 
				
			||||||
@@ -66,16 +80,31 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { ref } from 'vue';
 | 
					import { computed, ref } from 'vue';
 | 
				
			||||||
import { useStyleStore } from '@/stores/style.store';
 | 
					import { useStyleStore } from '@/stores/style.store';
 | 
				
			||||||
 | 
					import { getErrorMessageIfThrows } from '@/utils/error';
 | 
				
			||||||
import { convertBase } from './integer-base-converter.model';
 | 
					import { convertBase } from './integer-base-converter.model';
 | 
				
			||||||
import InputCopyable from '../../components/InputCopyable.vue';
 | 
					import InputCopyable from '../../components/InputCopyable.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const styleStore = useStyleStore();
 | 
					const styleStore = useStyleStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const inputNumber = ref(42);
 | 
					const input = ref('42');
 | 
				
			||||||
const inputBase = ref(10);
 | 
					const inputBase = ref(10);
 | 
				
			||||||
const outputBase = ref(42);
 | 
					const outputBase = ref(42);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function errorlessConvert(...args: Parameters<typeof convertBase>) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    return convertBase(...args);
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    return '';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error = computed(() =>
 | 
				
			||||||
 | 
					  getErrorMessageIfThrows(() =>
 | 
				
			||||||
 | 
					    convertBase({ value: input.value, fromBase: inputBase.value, toBase: outputBase.value }),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="less" scoped>
 | 
					<style lang="less" scoped>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,5 +29,3 @@ const expression = ref('');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const result = computed(() => withDefaultOnError(() => evaluate(expression.value) ?? '', ''));
 | 
					const result = computed(() => withDefaultOnError(() => evaluate(expression.value) ?? '', ''));
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="less" scoped></style>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -95,5 +95,3 @@ const selectedExtension = ref(undefined);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const mimeTypeFound = computed(() => (selectedExtension.value ? extensionToMimeType[selectedExtension.value] : []));
 | 
					const mimeTypeFound = computed(() => (selectedExtension.value ? extensionToMimeType[selectedExtension.value] : []));
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="less" scoped></style>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,27 +1,10 @@
 | 
				
			|||||||
import { config } from '@/config';
 | 
					import { config } from '@/config';
 | 
				
			||||||
import type { Component } from 'vue';
 | 
					import type { Tool } from './tools.types';
 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface ITool {
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
  path: string;
 | 
					 | 
				
			||||||
  description: string;
 | 
					 | 
				
			||||||
  keywords: string[];
 | 
					 | 
				
			||||||
  component: () => Promise<Component>;
 | 
					 | 
				
			||||||
  icon: Component;
 | 
					 | 
				
			||||||
  redirectFrom?: string[];
 | 
					 | 
				
			||||||
  isNew: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface ToolCategory {
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
  icon: Component;
 | 
					 | 
				
			||||||
  components: ITool[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
 | 
					type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function defineTool(
 | 
					export function defineTool(
 | 
				
			||||||
  tool: WithOptional<ITool, 'isNew'>,
 | 
					  tool: WithOptional<Tool, 'isNew'>,
 | 
				
			||||||
  { newTools }: { newTools: string[] } = { newTools: config.tools.newTools },
 | 
					  { newTools }: { newTools: string[] } = { newTools: config.tools.newTools },
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const isNew = newTools.includes(tool.name);
 | 
					  const isNew = newTools.includes(tool.name);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										44
									
								
								src/tools/tools.store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/tools/tools.store.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					import { get, useStorage, type MaybeRef } from '@vueuse/core';
 | 
				
			||||||
 | 
					import { defineStore } from 'pinia';
 | 
				
			||||||
 | 
					import type { Ref } from 'vue';
 | 
				
			||||||
 | 
					import { toolsWithCategory } from './index';
 | 
				
			||||||
 | 
					import type { Tool, ToolWithCategory } from './tools.types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useToolStore = defineStore('tools', {
 | 
				
			||||||
 | 
					  state: () => ({
 | 
				
			||||||
 | 
					    favoriteToolsName: useStorage('favoriteToolsName', []) as Ref<string[]>,
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  getters: {
 | 
				
			||||||
 | 
					    favoriteTools(state) {
 | 
				
			||||||
 | 
					      return state.favoriteToolsName
 | 
				
			||||||
 | 
					        .map((favoriteName) => toolsWithCategory.find(({ name }) => name === favoriteName))
 | 
				
			||||||
 | 
					        .filter(Boolean) as ToolWithCategory[]; // cast because .filter(Boolean) does not remove undefined from type
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    notFavoriteTools(state): ToolWithCategory[] {
 | 
				
			||||||
 | 
					      return toolsWithCategory.filter((tool) => !state.favoriteToolsName.includes(tool.name));
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tools(): ToolWithCategory[] {
 | 
				
			||||||
 | 
					      return toolsWithCategory;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    newTools(): ToolWithCategory[] {
 | 
				
			||||||
 | 
					      return this.tools.filter(({ isNew }) => isNew);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  actions: {
 | 
				
			||||||
 | 
					    addToolToFavorites({ tool }: { tool: MaybeRef<Tool> }) {
 | 
				
			||||||
 | 
					      this.favoriteToolsName.push(get(tool).name);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    removeToolFromFavorites({ tool }: { tool: MaybeRef<Tool> }) {
 | 
				
			||||||
 | 
					      this.favoriteToolsName = this.favoriteToolsName.filter((name) => get(tool).name !== name);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isToolFavorite({ tool }: { tool: MaybeRef<Tool> }) {
 | 
				
			||||||
 | 
					      return this.favoriteToolsName.includes(get(tool).name);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/tools/tools.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/tools/tools.types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					import type { Component } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Tool = {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  path: string;
 | 
				
			||||||
 | 
					  description: string;
 | 
				
			||||||
 | 
					  keywords: string[];
 | 
				
			||||||
 | 
					  component: () => Promise<Component>;
 | 
				
			||||||
 | 
					  icon: Component;
 | 
				
			||||||
 | 
					  redirectFrom?: string[];
 | 
				
			||||||
 | 
					  isNew: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ToolCategory = {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  components: Tool[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ToolWithCategory = Tool & { category: string };
 | 
				
			||||||
							
								
								
									
										29
									
								
								src/utils/error.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/utils/error.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					import { describe, expect, it } from 'vitest';
 | 
				
			||||||
 | 
					import { getErrorMessageIfThrows } from './error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('error util', () => {
 | 
				
			||||||
 | 
					  describe('getErrorMessageIfThrows', () => {
 | 
				
			||||||
 | 
					    it('get an error message if the callback throws, undefined instead', () => {
 | 
				
			||||||
 | 
					      expect(
 | 
				
			||||||
 | 
					        getErrorMessageIfThrows(() => {
 | 
				
			||||||
 | 
					          throw 'message';
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ).to.equal('message');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(
 | 
				
			||||||
 | 
					        getErrorMessageIfThrows(() => {
 | 
				
			||||||
 | 
					          throw new Error('message');
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ).to.equal('message');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(
 | 
				
			||||||
 | 
					        getErrorMessageIfThrows(() => {
 | 
				
			||||||
 | 
					          throw { message: 'message' };
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      ).to.equal('message');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // eslint-disable-next-line @typescript-eslint/no-empty-function
 | 
				
			||||||
 | 
					      expect(getErrorMessageIfThrows(() => {})).to.equal(undefined);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										24
									
								
								src/utils/error.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/utils/error.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					import _ from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { getErrorMessageIfThrows };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getErrorMessageIfThrows(cb: () => unknown) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    cb();
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    if (_.isString(err)) {
 | 
				
			||||||
 | 
					      return err;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (_.isError(err)) {
 | 
				
			||||||
 | 
					      return err.message;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (_.isObject(err) && _.has(err, 'message')) {
 | 
				
			||||||
 | 
					      return (err as { message: string }).message;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 'An error as occurred.';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user