Compare commits

...

7 Commits

Author SHA1 Message Date
Corentin Thomasset
b2614990e3 chore(version): release 2023.11.02-7d94e11 2023-11-02 09:11:26 +01:00
Corentin Thomasset
79646375f3 docs(changelog): update changelog for 2023.11.02-7d94e11 2023-11-02 09:11:26 +01:00
Andrii Bratanin
7d94e11cee fix(dockerfile): revert replacement of nginx image with non-privileged one (#716)
Fixes https://github.com/CorentinTh/it-tools/issues/714
2023-11-02 09:10:13 +01:00
swishkin
e87f4b1837 refactor(math-evaluator): improved description
Improved description.
2023-11-01 23:07:51 +01:00
Corentin THOMASSET
e86fd96ae3 feat(i18n): language selector (#710) 2023-11-01 15:38:19 +01:00
Corentin THOMASSET
58de8970f5 refactor(math-evaluator): improved search and UX (#713)
* refactor(math-evaluator): added keywords for search

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

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

* feat(c-alert): added title

* refactor(composable): mutualized computedCatch

---------

Co-authored-by: code2933 <code2933@outlook.com>
2023-11-01 10:11:51 +00:00
22 changed files with 264 additions and 56 deletions

View File

@@ -2,6 +2,19 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## Version 2023.11.02-7d94e11
### Features
- **i18n**: language selector (#710) (e86fd96)
### Bug fixes
- **dockerfile**: revert replacement of nginx image with non-privileged one (#716) (7d94e11)
- **encryption**: alert on decryption error (#711) (02b0d0d)
### Refactoring
- **math-evaluator**: improved description (e87f4b1)
- **math-evaluator**: improved search and UX (#713) (58de897)
## Version 2023.11.01-e164afb
### Features

View File

@@ -10,8 +10,8 @@ COPY . .
RUN pnpm build
# production stage
FROM nginxinc/nginx-unprivileged:stable-alpine AS production-stage
FROM nginx:stable-alpine AS production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
CMD ["nginx", "-g", "daemon off;"]

2
components.d.ts vendored
View File

@@ -98,6 +98,7 @@ declare module '@vue/runtime-core' {
IconMdiRecord: typeof import('~icons/mdi/record')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconMdiSearch: typeof import('~icons/mdi/search')['default']
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
IconMdiVideo: typeof import('~icons/mdi/video')['default']
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
@@ -114,6 +115,7 @@ declare module '@vue/runtime-core' {
JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default']
ListConverter: typeof import('./src/tools/list-converter/list-converter.vue')['default']
LocaleSelector: typeof import('./src/modules/i18n/components/locale-selector.vue')['default']
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default']
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']

View File

@@ -28,7 +28,7 @@ home:
about:
h1: 'About IT-Tools'
h1p1: 'This wonderful website, made with ❤ by'
h1p2: ', aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share it to people you think may find it useful too and don''''t forget to bookmark it in your shortcut bar!'
h1p2: ", aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share it to people you think may find it useful too and don''t forget to bookmark it in your shortcut bar!"
h1p3: 'IT Tools is open-source (under the MIT license) and free, and will always be, but it costs me money to host and renew the domain name. If you want to support my work, and encourage me to add more tools, please consider supporting by'
h1p4: 'sponsoring me'
h2: Technologies
@@ -38,7 +38,7 @@ about:
h3p1: 'If you need a tool that is currently not present here, and you think can be useful, you are welcome to submit a feature request in the'
h3p2: 'issues section'
h3p3: 'in the GitHub repository.'
h3p4: 'And if you found a bug, or something doesn''''t work as expected, please file a bug report in the'
h3p4: "And if you found a bug, or something doesn''t work as expected, please file a bug report in the"
h3p5: 'issues section'
h3p6: 'in the GitHub repository.'
404:
@@ -48,4 +48,18 @@ about:
backHome: 'Back home'
toolCard:
new: New
search:
label: Search
tools:
categories:
favorite-tools: 'Your favorite tools'
crypto: Crypto
converter: Converter
web: Web
images and videos: 'Images & Videos'
development: Development
network: Network
math: Math
measurement: Measurement
text: Text
data: Data

View File

@@ -1,3 +1,49 @@
home:
categories:
newestTools: "Nouveaux outils"
newestTools: 'Les nouveaux outils'
favoriteTools: 'Vos outils favoris'
allTools: 'Tous les outils'
subtitle: 'Outils pour les développeurs'
toggleMenu: 'Menu'
home: Accueil
uiLib: 'UI Lib'
buyMeACoffee: 'Soutenez IT-Tools'
follow:
title: 'Vous aimez it-tools ?'
p1: 'Soutenez-nous avec une star sur'
githubRepository: "le dépôt GitHub d'IT-Tools"
p2: 'ou suivez-nous sur'
twitterAccount: "le compte Twitter d'IT-Tools"
thankYou: 'Merci !'
nav:
github: 'Dépôt GitHub'
githubRepository: "Dépôt GitHub d'IT-Tools"
twitter: 'Compte Twitter'
twitterAccount: "Compte Twitter d'IT-Tools"
about: "À propos d'IT-Tools"
aboutLabel: 'À propos'
darkMode: 'Mode sombre'
lightMode: 'Mode clair'
mode: 'Basculer le mode sombre/clair'
404:
notFound: '404 Not Found'
sorry: "Désolé, cette page n'existe pas"
maybe: 'Peut-être que le cache fait des siennes, essayez de forcer le rafraîchissement ?'
backHome: "Retour à l'accueil"
toolCard:
new: Nouveau
search:
label: Rechercher
tools:
categories:
favorite-tools: 'Vos outils favoris'
crypto: Cryptographie
converter: Convertisseur
web: Web
images and videos: 'Images & Vidéos'
development: Développement
network: Réseau
math: Math
measurement: Mesure
text: Texte
data: Données

View File

@@ -1,6 +1,6 @@
{
"name": "it-tools",
"version": "2023.11.1-e164afb",
"version": "2023.11.2-7d94e11",
"description": "Collection of handy online tools for developers, with great UX. ",
"keywords": [
"productivity",

View File

@@ -11,6 +11,13 @@ const styleStore = useStyleStore();
const theme = computed(() => (styleStore.isDarkTheme ? darkTheme : null));
const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrides : lightThemeOverrides));
const { locale } = useI18n();
syncRef(
locale,
useStorage('locale', locale),
);
</script>
<template>

View File

@@ -36,7 +36,7 @@ const menuOptions = computed(() =>
tools: components.map(tool => ({
label: makeLabel(tool),
icon: makeIcon(tool),
key: tool.name,
key: tool.path,
})),
})),
);
@@ -62,7 +62,7 @@ const themeVars = useThemeVars();
<n-menu
class="menu"
:value="route.name as string"
:value="route.path"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="tools"

View File

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

View File

@@ -4,10 +4,10 @@ import { NIcon, useThemeVars } from 'naive-ui';
import { RouterLink } from 'vue-router';
import { Heart, Home2, Menu2 } from '@vicons/tabler';
import { storeToRefs } from 'pinia';
import HeroGradient from '../assets/hero-gradient.svg?component';
import MenuLayout from '../components/MenuLayout.vue';
import NavbarButtons from '../components/NavbarButtons.vue';
import { toolsByCategory } from '@/tools';
import { useStyleStore } from '@/stores/style.store';
import { config } from '@/config';
import type { ToolCategory } from '@/tools/tools.types';
@@ -21,12 +21,14 @@ const version = config.app.version;
const commitSha = config.app.lastCommitSha.slice(0, 7);
const { tracker } = useTracker();
const { t } = useI18n();
const toolStore = useToolStore();
const { favoriteTools, toolsByCategory } = storeToRefs(toolStore);
const tools = computed<ToolCategory[]>(() => [
...(toolStore.favoriteTools.length > 0 ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }] : []),
...toolsByCategory,
...(favoriteTools.value.length > 0 ? [{ name: t('tools.categories.favorite-tools'), components: favoriteTools.value }] : []),
...toolsByCategory.value,
]);
</script>
@@ -47,8 +49,12 @@ const tools = computed<ToolCategory[]>(() => [
</RouterLink>
<div class="sider-content">
<div v-if="styleStore.isSmallScreen" flex justify-center>
<NavbarButtons />
<div v-if="styleStore.isSmallScreen" flex flex-col items-center>
<locale-selector w="90%" />
<div flex justify-center>
<NavbarButtons />
</div>
</div>
<CollapsibleToolMenu :tools-by-category="tools" />
@@ -108,6 +114,8 @@ const tools = computed<ToolCategory[]>(() => [
<command-palette />
<locale-selector v-if="!styleStore.isSmallScreen" />
<div>
<NavbarButtons v-if="!styleStore.isSmallScreen" />
</div>

View File

@@ -116,7 +116,7 @@ function activateOption(option: PaletteOption) {
<span flex items-center gap-3 op-40>
<icon-mdi-search />
Search...
{{ $t('search.label') }}
<span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline>
{{ isMac ? 'Cmd' : 'Ctrl' }}&nbsp;+&nbsp;K

View File

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

View File

@@ -31,7 +31,8 @@ const { t } = useI18n();
rel="noopener"
target="_blank"
:aria-label="$t('home.follow.twitterAccount')"
>Twitter</a>{{ $t('home.follow.thankYou') }}
>Twitter</a>.
{{ $t('home.follow.thankYou') }}
<n-icon :component="Heart" />
</ColoredCard>
</n-gi>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { AES, RC4, Rabbit, TripleDES, enc } from 'crypto-js';
import { computedCatch } from '@/composable/computed/catchedComputed';
const algos = { AES, TripleDES, Rabbit, RC4 };
@@ -11,9 +12,10 @@ const cypherOutput = computed(() => algos[cypherAlgo.value].encrypt(cypherInput.
const decryptInput = ref('U2FsdGVkX1/EC3+6P5dbbkZ3e1kQ5o2yzuU0NHTjmrKnLBEwreV489Kr0DIB+uBs');
const decryptAlgo = ref<keyof typeof algos>('AES');
const decryptSecret = ref('my secret key');
const decryptOutput = computed(() =>
algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8),
);
const [decryptOutput, decryptError] = computedCatch(() => algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8), {
defaultValue: '',
defaultErrorMessage: 'Unable to decrypt your text',
});
</script>
<template>
@@ -63,7 +65,11 @@ const decryptOutput = computed(() =>
/>
</div>
</div>
<c-alert v-if="decryptError" type="error" mt-12 title="Error while decrypting">
{{ decryptError }}
</c-alert>
<c-input-text
v-else
label="Your decrypted text:"
:value="decryptOutput"
placeholder="Your string hash"

View File

@@ -4,10 +4,13 @@ import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Math evaluator',
path: '/math-evaluator',
description: 'Evaluate math expression, like a calculator on steroid (you can use function like sqrt, cos, sin, abs, ...)',
description: 'A calculator for evaluating mathematical expressions. You can use functions like sqrt, cos, sin, abs, etc.',
keywords: [
'math',
'evaluator',
'calculator',
'expression',
'abs',
'acos',
'acosh',
'acot',
@@ -31,6 +34,7 @@ export const tool = defineTool({
'sech',
'sin',
'sinh',
'sqrt',
'tan',
'tanh',
],

View File

@@ -16,6 +16,9 @@ const result = computed(() => withDefaultOnError(() => evaluate(expression.value
multiline
placeholder="Your math expression (ex: 2*sqrt(6) )..."
raw-text
monospace
autofocus
autosize
/>
<c-card v-if="result !== ''" title="Result " mt-5>

View File

@@ -1,44 +1,57 @@
import { type MaybeRef, get, useStorage } from '@vueuse/core';
import { defineStore } from 'pinia';
import type { Ref } from 'vue';
import type { Tool, ToolWithCategory } from './tools.types';
import _ from 'lodash';
import type { Tool, ToolCategory, ToolWithCategory } from './tools.types';
import { toolsWithCategory } from './index';
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
},
export const useToolStore = defineStore('tools', () => {
const favoriteToolsName = useStorage('favoriteToolsName', []) as Ref<string[]>;
const { t } = useI18n();
notFavoriteTools(state): ToolWithCategory[] {
return toolsWithCategory.filter(tool => !state.favoriteToolsName.includes(tool.name));
},
const tools = computed<ToolWithCategory[]>(() => toolsWithCategory.map((tool) => {
const toolI18nKey = tool.path.replace(/\//g, '');
tools(): ToolWithCategory[] {
return toolsWithCategory;
},
return ({
...tool,
name: t(`tools.${toolI18nKey}.title`, tool.name),
description: t(`tools.${toolI18nKey}.description`, tool.description),
category: t(`tools.categories.${tool.category.toLowerCase()}`, tool.category),
});
}));
newTools(): ToolWithCategory[] {
return this.tools.filter(({ isNew }) => isNew);
},
},
const toolsByCategory = computed<ToolCategory[]>(() => {
return _.chain(tools.value)
.groupBy('category')
.map((components, name) => ({
name,
components,
}))
.value();
});
const favoriteTools = computed(() => {
return favoriteToolsName.value
.map(favoriteName => tools.value.find(({ name }) => name === favoriteName))
.filter(Boolean) as ToolWithCategory[]; // cast because .filter(Boolean) does not remove undefined from type
});
return {
tools,
favoriteTools,
toolsByCategory,
newTools: computed(() => tools.value.filter(({ isNew }) => isNew)),
actions: {
addToolToFavorites({ tool }: { tool: MaybeRef<Tool> }) {
this.favoriteToolsName.push(get(tool).name);
favoriteToolsName.value.push(get(tool).name);
},
removeToolFromFavorites({ tool }: { tool: MaybeRef<Tool> }) {
this.favoriteToolsName = this.favoriteToolsName.filter(name => get(tool).name !== name);
favoriteToolsName.value = favoriteToolsName.value.filter(name => get(tool).name !== name);
},
isToolFavorite({ tool }: { tool: MaybeRef<Tool> }) {
return this.favoriteToolsName.includes(get(tool).name);
return favoriteToolsName.value.includes(get(tool).name);
},
},
};
});

View File

@@ -1,11 +1,19 @@
<script lang="ts" setup>
const variants = ['warning'] as const;
const variants = ['warning', 'error'] as const;
</script>
<template>
<h2>Basic</h2>
<c-alert v-for="variant in variants" :key="variant" :type="variant" mb-4>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Magni reprehenderit itaque enim? Suscipit magni optio velit
quia, eveniet repellat pariatur quaerat laudantium dignissimos natus, beatae deleniti adipisci, atque necessitatibus
odio!
</c-alert>
<h2>With title</h2>
<c-alert v-for="variant in variants" :key="variant" :type="variant" title="This is the title" mb-4>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Magni reprehenderit itaque enim? Suscipit magni optio velit
quia, eveniet repellat pariatur quaerat laudantium dignissimos natus, beatae deleniti adipisci, atque necessitatibus
odio!
</c-alert>
</template>

View File

@@ -3,6 +3,7 @@ import { defineThemes } from '../theme/theme.models';
import { appThemes } from '../theme/themes';
import WarningIcon from '~icons/mdi/alert-circle-outline';
import ErrorIcon from '~icons/mdi/close-circle-outline';
export const { useTheme } = defineThemes({
dark: {
@@ -12,6 +13,12 @@ export const { useTheme } = defineThemes({
textColor: appThemes.dark.warning.color,
icon: WarningIcon,
},
error: {
backgroundColor: appThemes.dark.error.colorFaded,
borderColor: appThemes.dark.error.color,
textColor: appThemes.dark.error.color,
icon: ErrorIcon,
},
},
light: {
warning: {
@@ -20,5 +27,11 @@ export const { useTheme } = defineThemes({
textColor: darken(appThemes.light.warning.color, 40),
icon: WarningIcon,
},
error: {
backgroundColor: appThemes.light.error.colorFaded,
borderColor: appThemes.light.error.color,
textColor: darken(appThemes.light.error.color, 40),
icon: ErrorIcon,
},
},
});

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup>
import { useTheme } from './c-alert.theme';
const props = withDefaults(defineProps<{ type?: 'warning' }>(), { type: 'warning' });
const { type } = toRefs(props);
const props = withDefaults(defineProps<{ type?: 'warning'; title?: string }>(), { type: 'warning', title: undefined });
const { type, title } = toRefs(props);
const theme = useTheme();
const variantTheme = computed(() => theme.value[type.value]);
@@ -17,6 +17,9 @@ const variantTheme = computed(() => theme.value[type.value]);
</div>
<div class="c-alert--content">
<div v-if="title" class="c-alert--title" text-15px fw-600>
{{ title }}
</div>
<slot />
</div>
</div>

View File

@@ -33,4 +33,19 @@ const value = ref('');
<c-select label="Label" label-position="left" label-align="left" mb-2 label-width="200px" />
<c-select label="Label" label-position="left" label-align="center" mb-2 label-width="200px" />
<c-select label="Label" label-position="left" label-align="right" mb-2 label-width="200px" />
<h2>Custom displayed value</h2>
<c-select v-model:value="value" :options="optionsA" mb-2>
<template #displayed-value>
<span class="font-bold lh-normal">Hello</span>
</template>
</c-select>
<c-select v-model:value="value" :options="optionsA">
<template #displayed-value>
<span lh-normal>
<icon-mdi-translate />
</span>
</template>
</c-select>
</template>

View File

@@ -150,13 +150,15 @@ function onSearchInput() {
@keydown="handleKeydown"
>
<div flex-1 truncate>
<input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full lh-normal color-current @input="onSearchInput">
<span v-else-if="selectedOption" lh-normal>
{{ selectedOption.label }}
</span>
<span v-else class="placeholder" lh-normal>
{{ placeholder ?? 'Select an option' }}
</span>
<slot name="displayed-value">
<input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full lh-normal color-current @input="onSearchInput">
<span v-else-if="selectedOption" lh-normal>
{{ selectedOption.label }}
</span>
<span v-else class="placeholder" lh-normal>
{{ placeholder ?? 'Select an option' }}
</span>
</slot>
</div>
<icon-mdi-chevron-down class="chevron" />