Compare commits

..

6 Commits

Author SHA1 Message Date
Corentin Thomasset
433d6eae5b chore(release): 2.12.0 2022-08-24 00:17:40 +02:00
Corentin Thomasset
07a5fa51ec chore(release): 2.11.0 2022-08-24 00:16:59 +02:00
Corentin Thomasset
cc6070a166 feat(new-tool): added otp generator 2022-08-24 00:10:53 +02:00
Corentin Thomasset
741a3c25a9 feat(config): added tsx to allowed extension 2022-08-24 00:10:31 +02:00
Corentin Thomasset
a89c9bea42 refactor(useQRCode): switched args to MaybeRef 2022-08-24 00:09:59 +02:00
Corentin Thomasset
59ec6293b6 refactor: token generator can use a custom alphabet 2022-08-24 00:09:16 +02:00
24 changed files with 1104 additions and 1749 deletions

View File

@@ -2,56 +2,6 @@
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.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)
### Features
* **config:** added tsx to allowed extension ([ea5e7a7](https://github.com/CorentinTh/it-tools/commit/ea5e7a7fc7df1a3a912193912a6ab80a8a36a256))
* **date-converter:** added mongodb objectID format ([4ef2588](https://github.com/CorentinTh/it-tools/commit/4ef25887b9d874b8789bf8dbabd8aab92b4b1b03))
* **new-tool:** added otp generator ([5f16885](https://github.com/CorentinTh/it-tools/commit/5f168859238e9c3a8b8bbaf6b550c4b9bd163e00))
* **new-tool:** mime type to extension converter ([7c9b8ac](https://github.com/CorentinTh/it-tools/commit/7c9b8ac178967151a4f921ac26e8c2fe8d23b886))
### Bug Fixes
* **ui:** remove icon transparency overlap ([35a3760](https://github.com/CorentinTh/it-tools/commit/35a376077116dd65b21f9a0786d2ecfc14db6051))
### Refactors
* **otp-generator:** changed url ([7f22995](https://github.com/CorentinTh/it-tools/commit/7f229959d64b7a932f32753e3838d87a819a9192))
* token generator can use a custom alphabet ([83da6b7](https://github.com/CorentinTh/it-tools/commit/83da6b7ee9db29e40faf288f9627257aa7124038))
* **ui:** change sponsor button location and caption ([5d8f46a](https://github.com/CorentinTh/it-tools/commit/5d8f46abf8d5a10cc4650efc87b12a9a6c537fe5))
* **useQRCode:** switched args to MaybeRef ([7de6c86](https://github.com/CorentinTh/it-tools/commit/7de6c86f9ead8d7315614cc508dfee4fed90e9c2))
## [2.12.0](https://github.com/CorentinTh/it-tools/compare/v2.10.3...v2.12.0) (2022-08-23) ## [2.12.0](https://github.com/CorentinTh/it-tools/compare/v2.10.3...v2.12.0) (2022-08-23)

View File

@@ -1,6 +1,6 @@
{ {
"name": "it-tools", "name": "it-tools",
"version": "2.15.0", "version": "2.12.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",
@@ -36,53 +36,52 @@
"@vicons/material": "^0.12.0", "@vicons/material": "^0.12.0",
"@vicons/tabler": "^0.12.0", "@vicons/tabler": "^0.12.0",
"@vueuse/core": "^8.9.4", "@vueuse/core": "^8.9.4",
"@vueuse/head": "^0.7.13", "@vueuse/head": "^0.7.9",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"change-case": "^4.1.2", "change-case": "^4.1.2",
"colord": "^2.9.3", "colord": "^2.9.3",
"cron-validator": "^1.3.1", "cron-validator": "^1.3.1",
"cronstrue": "^2.15.0", "cronstrue": "^2.11.0",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"date-fns": "^2.29.3", "date-fns": "^2.29.1",
"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",
"mathjs": "^10.6.4", "mathjs": "^10.6.4",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"naive-ui": "^2.33.5", "naive-ui": "^2.32.1",
"pinia": "^2.0.23", "pinia": "^2.0.18",
"plausible-tracker": "^0.3.8", "plausible-tracker": "^0.3.8",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"randombytes": "^2.1.0", "randombytes": "^2.1.0",
"sql-formatter": "^8.2.0", "sql-formatter": "^8.2.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"vue": "^3.2.45", "vue": "^3.2.37",
"vue-router": "^4.1.6" "vue-router": "^4.1.3"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.2.0", "@rushstack/eslint-patch": "^1.1.4",
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/jsdom": "^16.2.15", "@types/jsdom": "^16.2.15",
"@types/lodash": "^4.14.188", "@types/lodash": "^4.14.183",
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",
"@types/node": "^16.18.3", "@types/node": "^16.11.49",
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.4.3",
"@types/randombytes": "^2.0.0", "@types/randombytes": "^2.0.0",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@typescript-eslint/parser": "^5.42.1", "@typescript-eslint/parser": "^5.33.1",
"@vitejs/plugin-vue": "^2.3.4", "@vitejs/plugin-vue": "^2.3.4",
"@vitejs/plugin-vue-jsx": "^1.3.10", "@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^10.0.0", "@vue/eslint-config-typescript": "^10.0.0",
"@vue/test-utils": "^2.2.2", "@vue/test-utils": "^2.0.2",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.1.3",
"c8": "^7.12.0", "c8": "^7.12.0",
"eslint": "^8.27.0", "eslint": "^8.22.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.2", "eslint-import-resolver-typescript": "^3.4.2",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-vue": "^8.7.1", "eslint-plugin-vue": "^8.7.1",
"jsdom": "^19.0.0", "jsdom": "^19.0.0",
@@ -94,7 +93,7 @@
"vite": "^2.9.15", "vite": "^2.9.15",
"vite-plugin-md": "^0.12.4", "vite-plugin-md": "^0.12.4",
"vite-plugin-pwa": "^0.11.13", "vite-plugin-pwa": "^0.11.13",
"vite-svg-loader": "^3.6.0", "vite-svg-loader": "^3.4.0",
"vitest": "^0.13.1", "vitest": "^0.13.1",
"vue-tsc": "^0.31.4", "vue-tsc": "^0.31.4",
"workbox-window": "^6.5.4" "workbox-window": "^6.5.4"

2197
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,28 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useFuzzySearch } from '@/composable/fuzzySearch';
import { tools } from '@/tools'; import { tools } from '@/tools';
import type { ITool } from '@/tools/tool';
import { SearchRound } from '@vicons/material'; import { SearchRound } from '@vicons/material';
import { useMagicKeys, whenever } from '@vueuse/core'; import { useMagicKeys, whenever } from '@vueuse/core';
import { computed, h, ref } from 'vue'; import { deburr } from 'lodash';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import SearchBarItem from './SearchBarItem.vue';
const router = useRouter(); const router = useRouter();
const queryString = ref(''); const queryString = ref('');
const { searchResult } = useFuzzySearch({ const cleanString = (s: string) => deburr(s.trim().toLowerCase());
search: queryString,
data: tools,
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
});
const toolToOption = (tool: ITool) => ({ label: tool.name, value: tool.path, tool }); const searchableTools = tools.map(({ name, description, keywords, path }) => ({
searchableText: [name, description, ...keywords].map(cleanString).join(' '),
path,
name,
}));
const options = computed(() => { const options = computed(() => {
if (queryString.value === '') { const query = cleanString(queryString.value);
return tools.map(toolToOption);
}
return searchResult.value.map(toolToOption); return searchableTools
.filter(({ searchableText }) => searchableText.includes(query))
.map(({ name, path }) => ({ label: name, value: path }));
}); });
function onSelect(path: string) { function onSelect(path: string) {
@@ -46,10 +44,6 @@ const keys = useMagicKeys({
whenever(keys.ctrl_k, () => { whenever(keys.ctrl_k, () => {
focusTarget.value.focus(); focusTarget.value.focus();
}); });
function renderOption({ tool }: { tool: ITool }) {
return h(SearchBarItem, { tool });
}
</script> </script>
<template> <template>
@@ -57,10 +51,8 @@ function renderOption({ tool }: { tool: ITool }) {
<n-auto-complete <n-auto-complete
v-model:value="queryString" v-model:value="queryString"
:options="options" :options="options"
:on-select="(value) => onSelect(String(value))" :input-props="{ autocomplete: 'disabled' }"
:render-label="renderOption" :on-select="onSelect"
:default-value="'aa'"
:get-show="() => true"
> >
<template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }"> <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
<n-input <n-input
@@ -69,7 +61,6 @@ function renderOption({ tool }: { tool: ITool }) {
clearable clearable
placeholder="Search a tool... [Ctrl + K]" placeholder="Search a tool... [Ctrl + K]"
:value="slotValue" :value="slotValue"
:input-props="{ autocomplete: 'disabled' }"
@input="handleInput" @input="handleInput"
@focus="handleFocus" @focus="handleFocus"
@blur="handleBlur" @blur="handleBlur"
@@ -83,4 +74,8 @@ function renderOption({ tool }: { tool: ITool }) {
</div> </div>
</template> </template>
<style lang="less" scoped></style> <style lang="less" scoped>
// ::v-deep(.n-input__border) {
// border: none;
// }
</style>

View File

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

View File

@@ -49,8 +49,7 @@ a {
} }
.icon { .icon {
opacity: 0.6; opacity: 0.7;
color: v-bind('theme.textColorBase');
} }
.title { .title {
@@ -58,8 +57,7 @@ a {
} }
.description { .description {
opacity: 0.6; opacity: 0.7;
color: v-bind('theme.textColorBase');
margin: 5px 0; margin: 5px 0;
} }
} }

View File

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

View File

@@ -134,24 +134,23 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({
<search-bar /> <search-bar />
<navbar-buttons v-if="!styleStore.isSmallScreen" />
<n-tooltip trigger="hover"> <n-tooltip trigger="hover">
<template #trigger> <template #trigger>
<n-button <n-button
round
type="primary" type="primary"
tag="a" tag="a"
href="https://github.com/sponsors/CorentinTh" href="https://github.com/sponsors/CorentinTh"
rel="noopener" rel="noopener"
target="_blank" target="_blank"
> >
Buy me a coffee <n-icon v-if="!styleStore.isSmallScreen" :component="Heart" style="margin-right: 5px" />
<n-icon v-if="!styleStore.isSmallScreen" :component="Heart" style="margin-left: 5px" /> Sponsor
</n-button> </n-button>
</template> </template>
Support IT Tools development ! Support IT Tools development !
</n-tooltip> </n-tooltip>
<navbar-buttons v-if="!styleStore.isSmallScreen" />
</div> </div>
<slot /> <slot />
</template> </template>

View File

@@ -53,11 +53,9 @@ 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,

View File

@@ -6,12 +6,6 @@ export const lightThemeOverrides: GlobalThemeOverrides = {
}, },
Layout: { color: '#f1f5f9' }, Layout: { color: '#f1f5f9' },
AutoComplete: {
peers: {
InternalSelectMenu: { height: '500px' },
},
},
}; };
export const darkThemeOverrides: GlobalThemeOverrides = { export const darkThemeOverrides: GlobalThemeOverrides = {
@@ -22,12 +16,6 @@ export const darkThemeOverrides: GlobalThemeOverrides = {
primaryColorSuppl: '#36AD6AFF', primaryColorSuppl: '#36AD6AFF',
}, },
AutoComplete: {
peers: {
InternalSelectMenu: { height: '500px', color: '#1e1e1e' },
},
},
Menu: { Menu: {
itemHeight: '32px', itemHeight: '32px',
}, },
@@ -42,9 +30,4 @@ export const darkThemeOverrides: GlobalThemeOverrides = {
color: '#1e1e1e', color: '#1e1e1e',
borderColor: 'transparent', borderColor: 'transparent',
}, },
Table: {
tdColor: '#1e1e1e',
thColor: '#353535',
},
}; };

View File

@@ -1,68 +0,0 @@
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');
});
});
});

View File

@@ -1,17 +0,0 @@
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('');
}

View File

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

View File

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

View File

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

View File

@@ -132,10 +132,5 @@ const formats: Format[] = [
fromDate: (date) => date.toUTCString(), fromDate: (date) => date.toUTCString(),
toDate, toDate,
}, },
{
name: 'Mongo ObjectID',
fromDate: (date) => Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000',
toDate: (objectId) => new Date(parseInt(objectId.substring(0, 8), 16) * 1000),
},
]; ];
</script> </script>

View File

@@ -1,8 +1,6 @@
import { LockOpen } from '@vicons/tabler'; import { LockOpen } from '@vicons/tabler';
import type { ToolCategory } from './tool'; import type { ToolCategory } from './tool';
import { tool as chmodCalculator } from './chmod-calculator';
import { tool as mimeTypes } from './mime-types';
import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator'; import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter'; import { tool as base64StringConverter } from './base64-string-converter';
@@ -67,7 +65,6 @@ export const toolsByCategory: ToolCategory[] = [
basicAuthGenerator, basicAuthGenerator,
metaTagGenerator, metaTagGenerator,
otpCodeGeneratorAndValidator, otpCodeGeneratorAndValidator,
mimeTypes,
], ],
}, },
{ {
@@ -78,7 +75,7 @@ export const toolsByCategory: ToolCategory[] = [
{ {
name: 'Development', name: 'Development',
icon: LockOpen, icon: LockOpen,
components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify, chmodCalculator], components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify],
}, },
{ {
name: 'Math', name: 'Math',

View File

@@ -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);

View File

@@ -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 v-model:value="input" style="width: 100%" :status="error ? 'error' : undefined" /> <n-input-number v-model:value="inputNumber" min="0" style="width: 100%" />
</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,65 +14,51 @@
<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 v-model:value="input" :status="error ? 'error' : undefined" /> <n-input-number v-model:value="inputNumber" min="0" />
<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 <input-copyable :value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 2 })" readonly />
: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 <input-copyable :value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 8 })" readonly />
: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="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })" :value="convertBase({ value: String(inputNumber), 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="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })" :value="convertBase({ value: String(inputNumber), 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="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })" :value="convertBase({ value: String(inputNumber), 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="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })" :value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: outputBase })"
readonly readonly
:placeholder="`Base ${outputBase} will be here...`"
/> />
</n-input-group> </n-input-group>
</n-card> </n-card>
@@ -80,31 +66,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { 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 input = ref('42'); const inputNumber = 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>

View File

@@ -1,11 +0,0 @@
import { World } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Mime types',
path: '/mime-types',
description: 'Convert mime types to extensions and vice-versa.',
keywords: ['mime', 'types', 'extension', 'content', 'type'],
component: () => import('./mime-types.vue'),
icon: World,
});

View File

@@ -1,99 +0,0 @@
<template>
<n-card>
<n-h2 style="margin-bottom: 0">Mime type to extension</n-h2>
<div style="opacity: 0.8">Now witch file extensions are associated to a mime-type</div>
<n-form-item>
<n-select
v-model:value="selectedMimeType"
filterable
:options="mimeToExtensionsOptions"
size="large"
placeholder="Select your mimetype here... (ex: application/pdf)"
/>
</n-form-item>
<div v-if="extensionsFound.length > 0">
Extensions of files with the <n-tag round :bordered="false">{{ selectedMimeType }}</n-tag> mime-type:
<div style="margin-top: 10px">
<n-tag
v-for="extension of extensionsFound"
:key="extension"
round
:bordered="false"
type="primary"
style="margin-right: 10px"
>
.{{ extension }}
</n-tag>
</div>
</div>
</n-card>
<n-card>
<n-h2 style="margin-bottom: 0">File extension to mime type</n-h2>
<div style="opacity: 0.8">Now witch mime type is associated to a file extension</div>
<n-form-item>
<n-select
v-model:value="selectedExtension"
filterable
:options="extensionToMimeTypeOptions"
size="large"
placeholder="Select your mimetype here... (ex: application/pdf)"
/>
</n-form-item>
<div v-if="selectedExtension">
Mime type associated to the extension <n-tag round :bordered="false">{{ selectedExtension }}</n-tag> file
extension:
<div style="margin-top: 10px">
<n-tag round :bordered="false" type="primary" style="margin-right: 10px">
{{ mimeTypeFound }}
</n-tag>
</div>
</div>
</n-card>
<div>
<n-table>
<thead>
<tr>
<th>Mime types</th>
<th>Extensions</th>
</tr>
</thead>
<tbody>
<tr v-for="{ mimeType, extensions } of mimeInfos" :key="mimeType">
<td>{{ mimeType }}</td>
<td>
<n-tag v-for="extension of extensions" :key="extension" round :bordered="false" style="margin-right: 10px">
.{{ extension }}
</n-tag>
</td>
</tr>
</tbody>
</n-table>
</div>
</template>
<script setup lang="ts">
import { types as extensionToMimeType, extensions as mimeTypeToExtension } from 'mime-types';
import { computed, ref } from 'vue';
const mimeInfos = Object.entries(mimeTypeToExtension).map(([mimeType, extensions]) => ({ mimeType, extensions }));
const mimeToExtensionsOptions = Object.keys(mimeTypeToExtension).map((label) => ({ label, value: label }));
const selectedMimeType = ref(undefined);
const extensionsFound = computed(() => (selectedMimeType.value ? mimeTypeToExtension[selectedMimeType.value] : []));
const extensionToMimeTypeOptions = Object.keys(extensionToMimeType).map((label) => {
const extension = `.${label}`;
return { label: extension, value: label };
});
const selectedExtension = ref(undefined);
const mimeTypeFound = computed(() => (selectedExtension.value ? extensionToMimeType[selectedExtension.value] : []));
</script>
<style lang="less" scoped></style>

View File

@@ -3,7 +3,7 @@ import { defineTool } from '../tool';
export const tool = defineTool({ export const tool = defineTool({
name: 'OTP code generator', name: 'OTP code generator',
path: '/otp-generator', path: '/otp-code-generator-and-validator',
description: 'Generate and validate time-based OTP (one time password) for multi-factor authentication.', description: 'Generate and validate time-based OTP (one time password) for multi-factor authentication.',
keywords: [ keywords: [
'otp', 'otp',

View File

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

View File

@@ -1,24 +0,0 @@
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.';
}
}