diff options
-rw-r--r-- | components.d.ts | 2 | ||||
-rw-r--r-- | locales/en.yml | 20 | ||||
-rw-r--r-- | locales/fr.yml | 48 | ||||
-rw-r--r-- | src/App.vue | 7 | ||||
-rw-r--r-- | src/components/CollapsibleToolMenu.vue | 4 | ||||
-rw-r--r-- | src/layouts/base.layout.vue | 18 | ||||
-rw-r--r-- | src/modules/command-palette/command-palette.vue | 2 | ||||
-rw-r--r-- | src/modules/i18n/components/locale-selector.vue | 28 | ||||
-rw-r--r-- | src/pages/Home.page.vue | 3 | ||||
-rw-r--r-- | src/tools/tools.store.ts | 65 | ||||
-rw-r--r-- | src/ui/c-select/c-select.demo.vue | 15 | ||||
-rw-r--r-- | src/ui/c-select/c-select.vue | 16 |
12 files changed, 182 insertions, 46 deletions
diff --git a/components.d.ts b/components.d.ts index 669e133..be80e56 100644 --- a/components.d.ts +++ b/components.d.ts @@ -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'] diff --git a/locales/en.yml b/locales/en.yml index 1f31b68..5500f8b 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -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 -
\ No newline at end of file +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 diff --git a/locales/fr.yml b/locales/fr.yml index 2846356..9670546 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -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 diff --git a/src/App.vue b/src/App.vue index fec26bf..8e65335 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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> diff --git a/src/components/CollapsibleToolMenu.vue b/src/components/CollapsibleToolMenu.vue index 3a025ba..9a73ddd 100644 --- a/src/components/CollapsibleToolMenu.vue +++ b/src/components/CollapsibleToolMenu.vue @@ -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" diff --git a/src/layouts/base.layout.vue b/src/layouts/base.layout.vue index 2bbb67f..950b01d 100644 --- a/src/layouts/base.layout.vue +++ b/src/layouts/base.layout.vue @@ -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> diff --git a/src/modules/command-palette/command-palette.vue b/src/modules/command-palette/command-palette.vue index 7531aac..bceef5c 100644 --- a/src/modules/command-palette/command-palette.vue +++ b/src/modules/command-palette/command-palette.vue @@ -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' }} + K diff --git a/src/modules/i18n/components/locale-selector.vue b/src/modules/i18n/components/locale-selector.vue new file mode 100644 index 0000000..29dc0e5 --- /dev/null +++ b/src/modules/i18n/components/locale-selector.vue @@ -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> diff --git a/src/pages/Home.page.vue b/src/pages/Home.page.vue index 7f34081..859418e 100644 --- a/src/pages/Home.page.vue +++ b/src/pages/Home.page.vue @@ -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> diff --git a/src/tools/tools.store.ts b/src/tools/tools.store.ts index 769b4d8..d952b7c 100644 --- a/src/tools/tools.store.ts +++ b/src/tools/tools.store.ts @@ -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); }, - }, + }; }); diff --git a/src/ui/c-select/c-select.demo.vue b/src/ui/c-select/c-select.demo.vue index ae553bb..f656c01 100644 --- a/src/ui/c-select/c-select.demo.vue +++ b/src/ui/c-select/c-select.demo.vue @@ -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> diff --git a/src/ui/c-select/c-select.vue b/src/ui/c-select/c-select.vue index fb34038..7b3607c 100644 --- a/src/ui/c-select/c-select.vue +++ b/src/ui/c-select/c-select.vue @@ -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" /> |