diff options
Diffstat (limited to 'src')
174 files changed, 3124 insertions, 2873 deletions
diff --git a/src/App.vue b/src/App.vue index 98199af..6bd9a36 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,7 @@ <script setup lang="ts"> import { computed } from 'vue'; -import { useRoute, RouterView } from 'vue-router'; -import { darkTheme, NGlobalStyle, NMessageProvider, NNotificationProvider } from 'naive-ui'; +import { RouterView, useRoute } from 'vue-router'; +import { NGlobalStyle, NMessageProvider, NNotificationProvider, darkTheme } from 'naive-ui'; import { darkThemeOverrides, lightThemeOverrides } from './themes'; import { layouts } from './layouts'; import { useStyleStore } from './stores/style.store'; @@ -16,14 +16,14 @@ const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrid <template> <n-config-provider :theme="theme" :theme-overrides="themeOverrides"> - <n-global-style /> - <n-message-provider placement="bottom"> - <n-notification-provider placement="bottom-right"> + <NGlobalStyle /> + <NMessageProvider placement="bottom"> + <NNotificationProvider placement="bottom-right"> <component :is="layout"> - <router-view /> + <RouterView /> </component> - </n-notification-provider> - </n-message-provider> + </NNotificationProvider> + </NMessageProvider> </n-config-provider> </template> diff --git a/src/components/CollapsibleToolMenu.vue b/src/components/CollapsibleToolMenu.vue index b766255..de58ca1 100644 --- a/src/components/CollapsibleToolMenu.vue +++ b/src/components/CollapsibleToolMenu.vue @@ -1,39 +1,11 @@ -<template> - <div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name"> - <n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })"> - <n-icon :component="ChevronRight" :class="{ rotated: isCollapsed }" size="16" /> - - <span> - {{ name }} - </span> - </n-text> - - <n-collapse-transition :show="!isCollapsed"> - <div class="menu-wrapper"> - <div class="toggle-bar" @click="toggleCategoryCollapse({ name })" /> - - <n-menu - class="menu" - :value="(route.name as string)" - :collapsed-width="64" - :collapsed-icon-size="22" - :options="tools" - :indent="8" - :default-expand-all="true" - /> - </div> - </n-collapse-transition> - </div> -</template> - <script setup lang="ts"> -import type { Tool, ToolCategory } from '@/tools/tools.types'; import { ChevronRight } from '@vicons/tabler'; import { useStorage } from '@vueuse/core'; import { useThemeVars } from 'naive-ui'; -import { toRefs, computed, h } from 'vue'; +import { computed, h, toRefs } from 'vue'; import { RouterLink, useRoute } from 'vue-router'; import MenuIconItem from './MenuIconItem.vue'; +import type { Tool, ToolCategory } from '@/tools/tools.types'; const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] }); const { toolsByCategory } = toRefs(props); @@ -49,8 +21,8 @@ const collapsedCategories = useStorage<Record<string, boolean>>( { deep: true, serializer: { - read: (v) => (v ? JSON.parse(v) : null), - write: (v) => JSON.stringify(v), + read: v => (v ? JSON.parse(v) : null), + write: v => JSON.stringify(v), }, }, ); @@ -61,9 +33,9 @@ function toggleCategoryCollapse({ name }: { name: string }) { const menuOptions = computed(() => toolsByCategory.value.map(({ name, components }) => ({ - name: name, + name, isCollapsed: collapsedCategories.value[name], - tools: components.map((tool) => ({ + tools: components.map(tool => ({ label: makeLabel(tool), icon: makeIcon(tool), key: tool.name, @@ -74,6 +46,34 @@ const menuOptions = computed(() => const themeVars = useThemeVars(); </script> +<template> + <div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name"> + <n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })"> + <n-icon :component="ChevronRight" :class="{ rotated: isCollapsed }" size="16" /> + + <span> + {{ name }} + </span> + </n-text> + + <n-collapse-transition :show="!isCollapsed"> + <div class="menu-wrapper"> + <div class="toggle-bar" @click="toggleCategoryCollapse({ name })" /> + + <n-menu + class="menu" + :value="route.name as string" + :collapsed-width="64" + :collapsed-icon-size="22" + :options="tools" + :indent="8" + :default-expand-all="true" + /> + </div> + </n-collapse-transition> + </div> +</template> + <style scoped lang="less"> .category-name { font-size: 0.93em; diff --git a/src/components/ColoredCard.vue b/src/components/ColoredCard.vue index 9aaf167..d6374da 100644 --- a/src/components/ColoredCard.vue +++ b/src/components/ColoredCard.vue @@ -1,3 +1,10 @@ +<script setup lang="ts"> +import { type Component, toRefs } from 'vue'; + +const props = defineProps<{ icon: Component; title: string }>(); +const { icon, title } = toRefs(props); +</script> + <template> <c-card class="colored-card"> <n-icon class="icon" size="40" :component="icon" /> @@ -13,13 +20,6 @@ </c-card> </template> -<script setup lang="ts"> -import { toRefs, type Component } from 'vue'; - -const props = defineProps<{ icon: Component; title: string }>(); -const { icon, title } = toRefs(props); -</script> - <style lang="less" scoped> .colored-card { background: rgb(37, 99, 108); diff --git a/src/components/FavoriteButton.vue b/src/components/FavoriteButton.vue index 60b2a2b..16df18a 100644 --- a/src/components/FavoriteButton.vue +++ b/src/components/FavoriteButton.vue @@ -1,29 +1,13 @@ -<template> - <n-tooltip trigger="hover"> - <template #trigger> - <c-button - variant="text" - circle - :type="buttonType" - :style="{ opacity: isFavorite ? 1 : 0.2 }" - @click="toggleFavorite" - > - <n-icon :component="FavoriteFilled" /> - </c-button> - </template> - {{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }} - </n-tooltip> -</template> - <script setup lang="ts"> import { FavoriteFilled } from '@vicons/material'; +import { computed, toRefs } from 'vue'; import { useToolStore } from '@/tools/tools.store'; import type { Tool } from '@/tools/tools.types'; -import { computed, toRefs } from 'vue'; + +const props = defineProps<{ tool: Tool }>(); const toolStore = useToolStore(); -const props = defineProps<{ tool: Tool }>(); const { tool } = toRefs(props); const isFavorite = computed(() => toolStore.isToolFavorite({ tool })); @@ -40,3 +24,20 @@ function toggleFavorite(event: MouseEvent) { toolStore.addToolToFavorites({ tool }); } </script> + +<template> + <n-tooltip trigger="hover"> + <template #trigger> + <c-button + variant="text" + circle + :type="buttonType" + :style="{ opacity: isFavorite ? 1 : 0.2 }" + @click="toggleFavorite" + > + <n-icon :component="FavoriteFilled" /> + </c-button> + </template> + {{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }} + </n-tooltip> +</template> diff --git a/src/components/FormatTransformer.vue b/src/components/FormatTransformer.vue index 96f7798..cf62a9f 100644 --- a/src/components/FormatTransformer.vue +++ b/src/components/FormatTransformer.vue @@ -1,37 +1,17 @@ -<template> - <c-input-text - ref="inputElement" - v-model:value="input" - :placeholder="inputPlaceholder" - :label="inputLabel" - rows="20" - autosize - raw-text - multiline - test-id="input" - :validation-rules="inputValidationRules" - /> - - <div> - <div mb-5px>{{ outputLabel }}</div> - <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" /> - </div> -</template> - <script setup lang="ts"> -import type { UseValidationRule } from '@/composable/validation'; import _ from 'lodash'; +import type { UseValidationRule } from '@/composable/validation'; import CInputText from '@/ui/c-input-text/c-input-text.vue'; const props = withDefaults( defineProps<{ - transformer?: (v: string) => string; - inputValidationRules?: UseValidationRule<string>[]; - inputLabel?: string; - inputPlaceholder?: string; - inputDefault?: string; - outputLabel?: string; - outputLanguage?: string; + transformer?: (v: string) => string + inputValidationRules?: UseValidationRule<string>[] + inputLabel?: string + inputPlaceholder?: string + inputDefault?: string + outputLabel?: string + outputLanguage?: string }>(), { transformer: _.identity, @@ -44,8 +24,8 @@ const props = withDefaults( }, ); -const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } = - toRefs(props); +const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } + = toRefs(props); const inputElement = ref<typeof CInputText>(); @@ -53,4 +33,24 @@ const input = ref(inputDefault.value); const output = computed(() => transformer.value(input.value)); </script> -<style scoped></style> +<template> + <CInputText + ref="inputElement" + v-model:value="input" + :placeholder="inputPlaceholder" + :label="inputLabel" + rows="20" + autosize + raw-text + multiline + test-id="input" + :validation-rules="inputValidationRules" + /> + + <div> + <div mb-5px> + {{ outputLabel }} + </div> + <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" /> + </div> +</template> diff --git a/src/components/InputCopyable.vue b/src/components/InputCopyable.vue index cf7a42a..27c0657 100644 --- a/src/components/InputCopyable.vue +++ b/src/components/InputCopyable.vue @@ -1,20 +1,5 @@ -<template> - <c-input-text v-model:value="value"> - <template #suffix> - <n-tooltip trigger="hover"> - <template #trigger> - <c-button circle variant="text" size="small" @click="onCopyClicked"> - <icon-mdi-content-copy /> - </c-button> - </template> - {{ tooltipText }} - </n-tooltip> - </template> - </c-input-text> -</template> - <script setup lang="ts"> -import { useVModel, useClipboard } from '@vueuse/core'; +import { useClipboard, useVModel } from '@vueuse/core'; import { ref } from 'vue'; const props = defineProps<{ value: string }>(); @@ -34,3 +19,18 @@ function onCopyClicked() { }, 2000); } </script> + +<template> + <c-input-text v-model:value="value"> + <template #suffix> + <n-tooltip trigger="hover"> + <template #trigger> + <c-button circle variant="text" size="small" @click="onCopyClicked"> + <icon-mdi-content-copy /> + </c-button> + </template> + {{ tooltipText }} + </n-tooltip> + </template> + </c-input-text> +</template> diff --git a/src/components/MenuIconItem.vue b/src/components/MenuIconItem.vue index a08fe24..ed1b888 100644 --- a/src/components/MenuIconItem.vue +++ b/src/components/MenuIconItem.vue @@ -1,14 +1,7 @@ -<template> - <div class="menu-icon-item"> - <n-icon :component="tool.icon" /> - <div v-if="tool.isNew" class="badge"></div> - </div> -</template> - <script setup lang="ts"> -import type { Tool } from '@/tools/tools.types'; import { useThemeVars } from 'naive-ui'; import { toRefs } from 'vue'; +import type { Tool } from '@/tools/tools.types'; const props = defineProps<{ tool: Tool }>(); const { tool } = toRefs(props); @@ -16,6 +9,13 @@ const { tool } = toRefs(props); const theme = useThemeVars(); </script> +<template> + <div class="menu-icon-item"> + <n-icon :component="tool.icon" /> + <div v-if="tool.isNew" class="badge" /> + </div> +</template> + <style lang="less" scoped> .menu-icon-item { position: relative; diff --git a/src/components/MenuLayout.vue b/src/components/MenuLayout.vue index 177de2b..5659cbd 100644 --- a/src/components/MenuLayout.vue +++ b/src/components/MenuLayout.vue @@ -1,3 +1,12 @@ +<script setup lang="ts"> +import { computed, toRefs } from 'vue'; +import { useStyleStore } from '@/stores/style.store'; + +const styleStore = useStyleStore(); +const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore); +const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static')); +</script> + <template> <n-layout has-sider> <n-layout-sider @@ -19,15 +28,6 @@ </n-layout> </template> -<script setup lang="ts"> -import { useStyleStore } from '@/stores/style.store'; -import { toRefs, computed } from 'vue'; - -const styleStore = useStyleStore(); -const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore); -const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static')); -</script> - <style lang="less" scoped> .overlay { position: absolute; diff --git a/src/components/NavbarButtons.vue b/src/components/NavbarButtons.vue index 29d5417..2c7635c 100644 --- a/src/components/NavbarButtons.vue +++ b/src/components/NavbarButtons.vue @@ -1,3 +1,21 @@ +<script setup lang="ts"> +import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler'; +import { toRefs } from 'vue'; +import { useStyleStore } from '@/stores/style.store'; +import { useThemeStore } from '@/ui/theme/theme.store'; + +const styleStore = useStyleStore(); +const { isDarkTheme } = toRefs(styleStore); + +const themeStore = useThemeStore(); + +function toggleDarkTheme() { + isDarkTheme.value = !isDarkTheme.value; + + themeStore.toggleTheme(); +} +</script> + <template> <n-tooltip trigger="hover"> <template #trigger> @@ -51,24 +69,6 @@ </n-tooltip> </template> -<script setup lang="ts"> -import { useStyleStore } from '@/stores/style.store'; -import { useThemeStore } from '@/ui/theme/theme.store'; -import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler'; -import { toRefs } from 'vue'; - -const styleStore = useStyleStore(); -const { isDarkTheme } = toRefs(styleStore); - -const themeStore = useThemeStore(); - -function toggleDarkTheme() { - isDarkTheme.value = !isDarkTheme.value; - - themeStore.toggleTheme(); -} -</script> - <style lang="less" scoped> .n-button { &:not(:last-child) { diff --git a/src/components/SearchBar.vue b/src/components/SearchBar.vue index e69d817..0f4e663 100644 --- a/src/components/SearchBar.vue +++ b/src/components/SearchBar.vue @@ -1,14 +1,14 @@ <script lang="ts" setup> -import { useFuzzySearch } from '@/composable/fuzzySearch'; -import { useTracker } from '@/modules/tracker/tracker.services'; -import { tools } from '@/tools'; -import type { Tool } from '@/tools/tools.types'; import { SearchRound } from '@vicons/material'; import { useMagicKeys, whenever } from '@vueuse/core'; -import type { NInput } from 'naive-ui'; +import { NInput } from 'naive-ui'; import { computed, h, ref } from 'vue'; import { useRouter } from 'vue-router'; import SearchBarItem from './SearchBarItem.vue'; +import type { Tool } from '@/tools/tools.types'; +import { tools } from '@/tools'; +import { useTracker } from '@/modules/tracker/tracker.services'; +import { useFuzzySearch } from '@/composable/fuzzySearch'; const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool }); @@ -20,6 +20,12 @@ const inputEl = ref<HTMLElement>(); const displayDropDown = ref(true); const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac')); +const { searchResult } = useFuzzySearch({ + search: queryString, + data: tools, + options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] }, +}); + const options = computed(() => { if (queryString.value === '') { return tools.map(toolToOption); @@ -28,12 +34,6 @@ const options = computed(() => { return searchResult.value.map(toolToOption); }); -const { searchResult } = useFuzzySearch({ - search: queryString, - data: tools, - options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] }, -}); - const keys = useMagicKeys({ passive: false, onEventFired(e) { @@ -83,13 +83,13 @@ function onFocus() { :options="options" :on-select="(value: string | number) => onSelect(String(value))" :render-label="renderOption" - :default-value="'aa'" + default-value="aa" :get-show="() => displayDropDown" :on-focus="onFocus" @update:value="() => (displayDropDown = true)" > <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }"> - <n-input + <NInput ref="inputEl" round clearable @@ -103,7 +103,7 @@ function onFocus() { <template #prefix> <n-icon :component="SearchRound" /> </template> - </n-input> + </NInput> </template> </n-auto-complete> </div> diff --git a/src/components/SearchBarItem.vue b/src/components/SearchBarItem.vue index ca79268..69d02e6 100644 --- a/src/components/SearchBarItem.vue +++ b/src/components/SearchBarItem.vue @@ -1,6 +1,6 @@ <script lang="ts" setup> -import type { Tool } from '@/tools/tools.types'; import { toRefs } from 'vue'; +import type { Tool } from '@/tools/tools.types'; const props = defineProps<{ tool: Tool }>(); const { tool } = toRefs(props); @@ -11,8 +11,12 @@ const { tool } = toRefs(props); <n-icon class="icon" :component="tool.icon" /> <div> - <div class="name">{{ tool.name }}</div> - <div class="description">{{ tool.description }}</div> + <div class="name"> + {{ tool.name }} + </div> + <div class="description"> + {{ tool.description }} + </div> </div> </div> </template> diff --git a/src/components/SpanCopyable.vue b/src/components/SpanCopyable.vue index caac35d..c753d2e 100644 --- a/src/components/SpanCopyable.vue +++ b/src/components/SpanCopyable.vue @@ -1,12 +1,3 @@ -<template> - <n-tooltip trigger="hover"> - <template #trigger> - <span class="value" @click="handleClick">{{ value }}</span> - </template> - {{ tooltipText }} - </n-tooltip> -</template> - <script setup lang="ts"> import { useClipboard } from '@vueuse/core'; import { ref, toRefs } from 'vue'; @@ -27,6 +18,15 @@ function handleClick() { } </script> +<template> + <n-tooltip trigger="hover"> + <template #trigger> + <span class="value" @click="handleClick">{{ value }}</span> + </template> + {{ tooltipText }} + </n-tooltip> +</template> + <style scoped lang="less"> .value { cursor: pointer; diff --git a/src/components/TextareaCopyable.vue b/src/components/TextareaCopyable.vue index 17f90e2..2381856 100644 --- a/src/components/TextareaCopyable.vue +++ b/src/components/TextareaCopyable.vue @@ -1,32 +1,3 @@ -<template> - <div style="overflow-x: hidden; width: 100%"> - <c-card class="result-card"> - <n-scrollbar - x-scrollable - trigger="none" - :style="height ? `min-height: ${height - 40 /* card padding */ + 10 /* negative margin compensation */}px` : ''" - > - <n-config-provider :hljs="hljs"> - <n-code :code="value" :language="language" :trim="false" data-test-id="area-content" /> - </n-config-provider> - </n-scrollbar> - <n-tooltip v-if="value" trigger="hover"> - <template #trigger> - <div class="copy-button" :class="[copyPlacement]"> - <c-button circle important:h-10 important:w-10 @click="onCopyClicked"> - <n-icon size="22" :component="Copy" /> - </c-button> - </div> - </template> - <span>{{ tooltipText }}</span> - </n-tooltip> - </c-card> - <div v-if="copyPlacement === 'outside'" mt-4 flex justify-center> - <c-button @click="onCopyClicked"> {{ tooltipText }} </c-button> - </div> - </div> -</template> - <script setup lang="ts"> import { Copy } from '@vicons/tabler'; import { useClipboard, useElementSize } from '@vueuse/core'; @@ -37,18 +8,13 @@ import xmlHljs from 'highlight.js/lib/languages/xml'; import yamlHljs from 'highlight.js/lib/languages/yaml'; import { ref, toRefs } from 'vue'; -hljs.registerLanguage('sql', sqlHljs); -hljs.registerLanguage('json', jsonHljs); -hljs.registerLanguage('html', xmlHljs); -hljs.registerLanguage('yaml', yamlHljs); - const props = withDefaults( defineProps<{ - value: string; - followHeightOf?: HTMLElement | null; - language?: string; - copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none'; - copyMessage?: string; + value: string + followHeightOf?: HTMLElement | null + language?: string + copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none' + copyMessage?: string }>(), { followHeightOf: null, @@ -57,8 +23,13 @@ const props = withDefaults( copyMessage: 'Copy to clipboard', }, ); +hljs.registerLanguage('sql', sqlHljs); +hljs.registerLanguage('json', jsonHljs); +hljs.registerLanguage('html', xmlHljs); +hljs.registerLanguage('yaml', yamlHljs); + const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); -const { height } = followHeightOf ? useElementSize(followHeightOf) : { height: ref(null) }; +const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) }; const { copy } = useClipboard({ source: value }); const tooltipText = ref(copyMessage.value); @@ -73,6 +44,37 @@ function onCopyClicked() { } </script> +<template> + <div style="overflow-x: hidden; width: 100%"> + <c-card class="result-card"> + <n-scrollbar + x-scrollable + trigger="none" + :style="height ? `min-height: ${height - 40 /* card padding */ + 10 /* negative margin compensation */}px` : ''" + > + <n-config-provider :hljs="hljs"> + <n-code :code="value" :language="language" :trim="false" data-test-id="area-content" /> + </n-config-provider> + </n-scrollbar> + <n-tooltip v-if="value" trigger="hover"> + <template #trigger> + <div class="copy-button" :class="[copyPlacement]"> + <c-button circle important:h-10 important:w-10 @click="onCopyClicked"> + <n-icon size="22" :component="Copy" /> + </c-button> + </div> + </template> + <span>{{ tooltipText }}</span> + </n-tooltip> + </c-card> + <div v-if="copyPlacement === 'outside'" mt-4 flex justify-center> + <c-button @click="onCopyClicked"> + {{ tooltipText }} + </c-button> + </div> + </div> +</template> + <style lang="less" scoped> ::v-deep(.n-scrollbar) { padding-bottom: 10px; diff --git a/src/components/ToolCard.vue b/src/components/ToolCard.vue index db67914..9ccaec7 100644 --- a/src/components/ToolCard.vue +++ b/src/components/ToolCard.vue @@ -1,3 +1,17 @@ +<script setup lang="ts"> +import { useThemeVars } from 'naive-ui'; +import { toRefs } from 'vue'; +import FavoriteButton from './FavoriteButton.vue'; +import { useAppTheme } from '@/ui/theme/themes'; +import type { Tool } from '@/tools/tools.types'; + +const props = defineProps<{ tool: Tool & { category: string } }>(); +const { tool } = toRefs(props); +const theme = useThemeVars(); + +const appTheme = useAppTheme(); +</script> + <template> <router-link :to="tool.path"> <c-card class="tool-card"> @@ -16,7 +30,7 @@ New </n-tag> - <favorite-button :tool="tool" /> + <FavoriteButton :tool="tool" /> </div> </div> <n-h3 class="title"> @@ -26,27 +40,13 @@ <div class="description"> <n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px"> {{ tool.description }} - <br /> + <br> </n-ellipsis> </div> </c-card> </router-link> </template> -<script setup lang="ts"> -import type { Tool } from '@/tools/tools.types'; -import { useThemeVars } from 'naive-ui'; -import { toRefs } from 'vue'; -import { useAppTheme } from '@/ui/theme/themes'; -import FavoriteButton from './FavoriteButton.vue'; - -const props = defineProps<{ tool: Tool & { category: string } }>(); -const { tool } = toRefs(props); -const theme = useThemeVars(); - -const appTheme = useAppTheme(); -</script> - <style lang="less" scoped> a { text-decoration: none; diff --git a/src/composable/computedRefreshable.ts b/src/composable/computedRefreshable.ts index 89ab734..8680d06 100644 --- a/src/composable/computedRefreshable.ts +++ b/src/composable/computedRefreshable.ts @@ -11,7 +11,8 @@ function computedRefreshable<T>(getter: () => T, { throttle }: { throttle?: numb if (throttle) { watchThrottled(getter, update, { throttle }); - } else { + } + else { watch(getter, update); } diff --git a/src/composable/copy.ts b/src/composable/copy.ts index af4921b..6c1eca0 100644 --- a/src/composable/copy.ts +++ b/src/composable/copy.ts @@ -1,4 +1,4 @@ -import { useClipboard, type MaybeRef, get } from '@vueuse/core'; +import { type MaybeRef, get, useClipboard } from '@vueuse/core'; import { useMessage } from 'naive-ui'; export function useCopy({ source, text = 'Copied to the clipboard' }: { source: MaybeRef<unknown>; text?: string }) { diff --git a/src/composable/downloadBase64.ts b/src/composable/downloadBase64.ts index 9348363..3904315 100644 --- a/src/composable/downloadBase64.ts +++ b/src/composable/downloadBase64.ts @@ -5,8 +5,8 @@ function getFileExtensionFromBase64({ base64String, defaultExtension = 'txt', }: { - base64String: string; - defaultExtension?: string; + base64String: string + defaultExtension?: string }) { const hasMimeType = base64String.match(/data:(.*?);base64/i); diff --git a/src/composable/fuzzySearch.ts b/src/composable/fuzzySearch.ts index b46f9de..66480f7 100644 --- a/src/composable/fuzzySearch.ts +++ b/src/composable/fuzzySearch.ts @@ -1,4 +1,4 @@ -import { get, type MaybeRef } from '@vueuse/core'; +import { type MaybeRef, get } from '@vueuse/core'; import Fuse from 'fuse.js'; import { computed } from 'vue'; @@ -9,9 +9,9 @@ function useFuzzySearch<Data>({ data, options = {}, }: { - search: MaybeRef<string>; - data: Data[]; - options?: Fuse.IFuseOptions<Data>; + search: MaybeRef<string> + data: Data[] + options?: Fuse.IFuseOptions<Data> }) { const fuse = new Fuse(data, options); diff --git a/src/composable/validation.test.ts b/src/composable/validation.test.ts index 0bcb51f..4464bea 100644 --- a/src/composable/validation.test.ts +++ b/src/composable/validation.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ import { describe, expect, it } from 'vitest'; import { isFalsyOrHasThrown } from './validation'; @@ -11,7 +10,7 @@ describe('useValidation', () => { expect(isFalsyOrHasThrown(() => {})).toBe(true); expect( isFalsyOrHasThrown(() => { - throw new Error(); + throw new Error('message'); }), ).toBe(true); }); diff --git a/src/composable/validation.ts b/src/composable/validation.ts index e7fc70c..472ca4b 100644 --- a/src/composable/validation.ts +++ b/src/composable/validation.ts @@ -1,45 +1,48 @@ -import { get, type MaybeRef } from '@vueuse/core'; +import { type MaybeRef, get } from '@vueuse/core'; import _ from 'lodash'; -import { reactive, watch, type Ref } from 'vue'; +import { type Ref, reactive, watch } from 'vue'; type ValidatorReturnType = unknown; export interface UseValidationRule<T> { - validator: (value: T) => ValidatorReturnType; - message: string; + validator: (value: T) => ValidatorReturnType + message: string } export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { try { const returnValue = cb(); - if (_.isNil(returnValue)) return true; + if (_.isNil(returnValue)) { + return true; + } return returnValue === false; - } catch (_) { + } + catch (_) { return true; } } -export type ValidationAttrs = { - feedback: string; - validationStatus: string | undefined; -}; +export interface ValidationAttrs { + feedback: string + validationStatus: string | undefined +} export function useValidation<T>({ source, rules, watch: watchRefs = [], }: { - source: Ref<T>; - rules: MaybeRef<UseValidationRule<T>[]>; - watch?: Ref<unknown>[]; + source: Ref<T> + rules: MaybeRef<UseValidationRule<T>[]> + watch?: Ref<unknown>[] }) { const state = reactive<{ - message: string; - status: undefined | 'error'; - isValid: boolean; - attrs: ValidationAttrs; + message: string + status: undefined | 'error' + isValid: boolean + attrs: ValidationAttrs }>({ message: '', status: undefined, diff --git a/src/layouts/base.layout.vue b/src/layouts/base.layout.vue index dff1535..e4626ff 100644 --- a/src/layouts/base.layout.vue +++ b/src/layouts/base.layout.vue @@ -2,7 +2,11 @@ import { NIcon, useThemeVars } from 'naive-ui'; import { computed } from 'vue'; import { RouterLink } from 'vue-router'; -import { Heart, Menu2, Home2 } from '@vicons/tabler'; +import { Heart, Home2, Menu2 } from '@vicons/tabler'; +import SearchBar from '../components/SearchBar.vue'; +import HeroGradient from '../assets/hero-gradient.svg?component'; +import MenuLayout from '../components/MenuLayout.vue'; +import NavbarButtons from '../components/NavbarButtons.vue'; import { toolsByCategory } from '@/tools'; import { useStyleStore } from '@/stores/style.store'; import { config } from '@/config'; @@ -10,10 +14,6 @@ import type { ToolCategory } from '@/tools/tools.types'; import { useToolStore } from '@/tools/tools.store'; import { useTracker } from '@/modules/tracker/tracker.services'; import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue'; -import SearchBar from '../components/SearchBar.vue'; -import HeroGradient from '../assets/hero-gradient.svg?component'; -import MenuLayout from '../components/MenuLayout.vue'; -import NavbarButtons from '../components/NavbarButtons.vue'; const themeVars = useThemeVars(); const styleStore = useStyleStore(); @@ -31,23 +31,27 @@ const tools = computed<ToolCategory[]>(() => [ </script> <template> - <menu-layout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }"> + <MenuLayout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }"> <template #sider> - <router-link to="/" class="hero-wrapper"> - <hero-gradient class="gradient" /> + <RouterLink to="/" class="hero-wrapper"> + <HeroGradient class="gradient" /> <div class="text-wrapper"> - <div class="title">IT - TOOLS</div> + <div class="title"> + IT - TOOLS + </div> <div class="divider" /> - <div class="subtitle">Handy tools for developers</div> + <div class="subtitle"> + Handy tools for developers + </div> </div> - </router-link> + </RouterLink> <div class="sider-content"> <div v-if="styleStore.isSmallScreen" flex justify-center> - <navbar-buttons /> + <NavbarButtons /> </div> - <collapsible-tool-menu :tools-by-category="tools" /> + <CollapsibleToolMenu :tools-by-category="tools" /> <div class="footer"> <div> @@ -71,7 +75,9 @@ const tools = computed<ToolCategory[]>(() => [ </div> <div> © {{ new Date().getFullYear() }} - <c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh"> Corentin Thomasset </c-link> + <c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh"> + Corentin Thomasset + </c-link> </div> </div> </div> @@ -86,21 +92,21 @@ const tools = computed<ToolCategory[]>(() => [ aria-label="Toggle menu" @click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed" > - <n-icon size="25" :component="Menu2" /> + <NIcon size="25" :component="Menu2" /> </c-button> <n-tooltip trigger="hover"> <template #trigger> <c-button to="/" circle variant="text" aria-label="Home"> - <n-icon size="25" :component="Home2" /> + <NIcon size="25" :component="Home2" /> </c-button> </template> Home </n-tooltip> - <search-bar /> + <SearchBar /> - <navbar-buttons v-if="!styleStore.isSmallScreen" /> + <NavbarButtons v-if="!styleStore.isSmallScreen" /> <n-tooltip trigger="hover"> <template #trigger> @@ -114,7 +120,7 @@ const tools = computed<ToolCategory[]>(() => [ @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" > Buy me a coffee - <n-icon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 /> + <NIcon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 /> </c-button> </template> ❤ Support IT Tools development ! @@ -122,7 +128,7 @@ const tools = computed<ToolCategory[]>(() => [ </div> <slot /> </template> - </menu-layout> + </MenuLayout> </template> <style lang="less" scoped> diff --git a/src/layouts/tool.layout.vue b/src/layouts/tool.layout.vue index ae385d0..1758cb1 100644 --- a/src/layouts/tool.layout.vue +++ b/src/layouts/tool.layout.vue @@ -3,9 +3,9 @@ import { useRoute } from 'vue-router'; import { useHead } from '@vueuse/head'; import type { HeadObject } from '@vueuse/head'; import { computed } from 'vue'; +import BaseLayout from './base.layout.vue'; import FavoriteButton from '@/components/FavoriteButton.vue'; import type { Tool } from '@/tools/tools.types'; -import BaseLayout from './base.layout.vue'; const route = useRoute(); @@ -26,7 +26,7 @@ useHead(head); </script> <template> - <base-layout> + <BaseLayout> <div class="tool-layout"> <div class="tool-header"> <div flex flex-nowrap items-center justify-between> @@ -35,7 +35,7 @@ useHead(head); </n-h1> <div> - <favorite-button :tool="{name: route.meta.name} as Tool" /> + <FavoriteButton :tool="{ name: route.meta.name } as Tool" /> </div> </div> @@ -50,7 +50,7 @@ useHead(head); <div class="tool-content"> <slot /> </div> - </base-layout> + </BaseLayout> </template> <style lang="less" scoped> diff --git a/src/main.ts b/src/main.ts index f063ef6..e23cb91 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,19 +1,19 @@ import { createApp } from 'vue'; import { createPinia } from 'pinia'; import { createHead } from '@vueuse/head'; -// eslint-disable-next-line import/no-unresolved + import { registerSW } from 'virtual:pwa-register'; import { plausible } from './plugins/plausible.plugin'; import 'virtual:uno.css'; -registerSW(); - import { naive } from './plugins/naive.plugin'; import App from './App.vue'; import router from './router'; +registerSW(); + const app = createApp(App); app.use(createPinia()); diff --git a/src/modules/tracker/tracker.services.ts b/src/modules/tracker/tracker.services.ts index bd3c0ec..266a11f 100644 --- a/src/modules/tracker/tracker.services.ts +++ b/src/modules/tracker/tracker.services.ts @@ -16,7 +16,7 @@ function useTracker() { const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible'); if (_.isNil(plausible)) { - throw new Error('Plausible must be instantiated'); + throw new TypeError('Plausible must be instantiated'); } const tracker = createTrackerService({ plausible }); diff --git a/src/pages/404.page.vue b/src/pages/404.page.vue index fb879aa..293a8f9 100644 --- a/src/pages/404.page.vue +++ b/src/pages/404.page.vue @@ -9,10 +9,18 @@ useHead({ title: 'Page not found - IT Tools' }); <div mt-20 flex flex-col items-center> <n-icon :component="Coffee" size="100" depth="3" /> - <n-h1 m-0 mt-3>404 Not Found</n-h1> - <n-text mt-4 block depth="3">Sorry, this page does not seem to exist</n-text> - <n-text mb-8 block depth="3">Maybe the cache is doing tricky things, try force-refreshing?</n-text> + <n-h1 m-0 mt-3> + 404 Not Found + </n-h1> + <n-text mt-4 block depth="3"> + Sorry, this page does not seem to exist + </n-text> + <n-text mb-8 block depth="3"> + Maybe the cache is doing tricky things, try force-refreshing? + </n-text> - <c-button to="/"> Back home </c-button> + <c-button to="/"> + Back home + </c-button> </div> </template> diff --git a/src/pages/About.vue b/src/pages/About.vue index 6ed4d08..cbf8964 100644 --- a/src/pages/About.vue +++ b/src/pages/About.vue @@ -1,6 +1,6 @@ <script setup lang="ts"> -import { useTracker } from '@/modules/tracker/tracker.services'; import { useHead } from '@vueuse/head'; +import { useTracker } from '@/modules/tracker/tracker.services'; useHead({ title: 'About - IT Tools' }); const { tracker } = useTracker(); @@ -11,7 +11,9 @@ const { tracker } = useTracker(); <n-h1>About</n-h1> <n-p> This wonderful website, made with ❤ by - <c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener"> Corentin Thomasset </c-link>, + <c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener"> + Corentin Thomasset + </c-link>, aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to share it to people you think may find it useful too and don't forget to pin it in your shortcut bar ! </n-p> @@ -25,8 +27,8 @@ const { tracker } = useTracker(); target="_blank" @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" > - sponsoring me </c-link - >. + sponsoring me + </c-link>. </n-p> <n-h2>Technologies</n-h2> diff --git a/src/pages/Home.page.vue b/src/pages/Home.page.vue index cd77e0c..01a7296 100644 --- a/src/pages/Home.page.vue +++ b/src/pages/Home.page.vue @@ -1,10 +1,10 @@ <script setup lang="ts"> -import { config } from '@/config'; -import { useToolStore } from '@/tools/tools.store'; import { Heart } from '@vicons/tabler'; import { useHead } from '@vueuse/head'; import ColoredCard from '../components/ColoredCard.vue'; import ToolCard from '../components/ToolCard.vue'; +import { useToolStore } from '@/tools/tools.store'; +import { config } from '@/config'; const toolStore = useToolStore(); @@ -16,25 +16,23 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); <div class="grid-wrapper"> <n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> <n-gi> - <colored-card title="You like it-tools?" :icon="Heart"> + <ColoredCard title="You like it-tools?" :icon="Heart"> Give us a star on <a href="https://github.com/CorentinTh/it-tools" rel="noopener" target="_blank" aria-label="IT-Tools' GitHub repository" - >GitHub</a - > + >GitHub</a> or follow us on <a href="https://twitter.com/ittoolsdottech" rel="noopener" target="_blank" aria-label="IT-Tools' Twitter account" - >Twitter</a - >! Thank you + >Twitter</a>! Thank you <n-icon :component="Heart" /> - </colored-card> + </ColoredCard> </n-gi> </n-grid> @@ -43,7 +41,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); <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" /> + <ToolCard :tool="tool" /> </n-gi> </n-grid> </div> @@ -53,7 +51,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); <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" /> + <ToolCard :tool="tool" /> </n-gi> </n-grid> </div> @@ -62,7 +60,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); <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" /> + <ToolCard :tool="tool" /> </transition> </n-gi> </n-grid> diff --git a/src/plugins/plausible.plugin.ts b/src/plugins/plausible.plugin.ts index 50a694d..cc8998e 100644 --- a/src/plugins/plausible.plugin.ts +++ b/src/plugins/plausible.plugin.ts @@ -1,8 +1,8 @@ -import { config } from '@/config'; import { noop } from 'lodash'; import Plausible from 'plausible-tracker'; import type { App } from 'vue'; +import { config } from '@/config'; function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> { return { @@ -15,11 +15,11 @@ function createPlausibleInstance({ config, }: { config: { - isTrackerEnabled: boolean; - domain: string; - apiHost: string; - trackLocalhost: boolean; - }; + isTrackerEnabled: boolean + domain: string + apiHost: string + trackLocalhost: boolean + } }) { if (config.isTrackerEnabled) { return Plausible(config); diff --git a/src/router.ts b/src/router.ts index 00c20c7..da0c4f1 100644 --- a/src/router.ts +++ b/src/router.ts @@ -15,7 +15,7 @@ const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({ const toolsRedirectRoutes = tools .filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0) .flatMap( - ({ path, redirectFrom }) => redirectFrom?.map((redirectSource) => ({ path: redirectSource, redirect: path })) ?? [], + ({ path, redirectFrom }) => redirectFrom?.map(redirectSource => ({ path: redirectSource, redirect: path })) ?? [], ); const router = createRouter({ diff --git a/src/stores/style.store.ts b/src/stores/style.store.ts index bd6559a..d70bd76 100644 --- a/src/stores/style.store.ts +++ b/src/stores/style.store.ts @@ -1,6 +1,6 @@ import { useMediaQuery, useStorage } from '@vueuse/core'; import { defineStore } from 'pinia'; -import { watch, type Ref } from 'vue'; +import { type Ref, watch } from 'vue'; export const useStyleStore = defineStore('style', { state: () => { @@ -8,7 +8,7 @@ export const useStyleStore = defineStore('style', { const isSmallScreen = useMediaQuery('(max-width: 700px)'); const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>; - watch(isSmallScreen, (v) => (isMenuCollapsed.value = v)); + watch(isSmallScreen, v => (isMenuCollapsed.value = v)); return { isDarkTheme, diff --git a/src/tools/base64-file-converter/base64-file-converter.vue b/src/tools/base64-file-converter/base64-file-converter.vue index de18ee3..d5bcb49 100644 --- a/src/tools/base64-file-converter/base64-file-converter.vue +++ b/src/tools/base64-file-converter/base64-file-converter.vue @@ -1,48 +1,12 @@ -<template> - <c-card title="Base64 to file"> - <c-input-text - v-model:value="base64Input" - multiline - placeholder="Put your base64 file string here..." - rows="5" - :validation="base64InputValidation" - mb-2 - /> - - <div flex justify-center> - <c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()"> - Download file - </c-button> - </div> - </c-card> - - <c-card title="File to base64"> - <n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image"> - <n-upload-dragger> - <div mb-2> - <n-icon size="35" :depth="3" :component="Upload" /> - </div> - <n-text style="font-size: 14px"> Click or drag a file to this area to upload </n-text> - </n-upload-dragger> - </n-upload> - - <c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 /> - - <div flex justify-center> - <c-button @click="copyFileBase64()"> Copy </c-button> - </div> - </c-card> -</template> - <script setup lang="ts"> +import { Upload } from '@vicons/tabler'; +import { useBase64 } from '@vueuse/core'; +import type { UploadFileInfo } from 'naive-ui'; +import { type Ref, ref } from 'vue'; import { useCopy } from '@/composable/copy'; import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; import { useValidation } from '@/composable/validation'; import { isValidBase64 } from '@/utils/base64'; -import { Upload } from '@vicons/tabler'; -import { useBase64 } from '@vueuse/core'; -import type { UploadFileInfo } from 'naive-ui'; -import { ref, type Ref } from 'vue'; const base64Input = ref(''); const { download } = useDownloadFileFromBase64({ source: base64Input }); @@ -51,17 +15,20 @@ const base64InputValidation = useValidation({ rules: [ { message: 'Invalid base 64 string', - validator: (value) => isValidBase64(value.trim()), + validator: value => isValidBase64(value.trim()), }, ], }); function downloadFile() { - if (!base64InputValidation.isValid) return; + if (!base64InputValidation.isValid) { + return; + } try { download(); - } catch (_) { + } + catch (_) { // } } @@ -79,6 +46,46 @@ async function onUpload({ file: { file } }: { file: UploadFileInfo }) { } </script> +<template> + <c-card title="Base64 to file"> + <c-input-text + v-model:value="base64Input" + multiline + placeholder="Put your base64 file string here..." + rows="5" + :validation="base64InputValidation" + mb-2 + /> + + <div flex justify-center> + <c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()"> + Download file + </c-button> + </div> + </c-card> + + <c-card title="File to base64"> + <n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image"> + <n-upload-dragger> + <div mb-2> + <n-icon size="35" :depth="3" :component="Upload" /> + </div> + <n-text style="font-size: 14px"> + Click or drag a file to this area to upload + </n-text> + </n-upload-dragger> + </n-upload> + + <c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 /> + + <div flex justify-center> + <c-button @click="copyFileBase64()"> + Copy + </c-button> + </div> + </c-card> +</template> + <style lang="less" scoped> ::v-deep(.n-upload-trigger) { width: 100%; diff --git a/src/tools/base64-file-converter/index.ts b/src/tools/base64-file-converter/index.ts index 2d93d02..c27e34e 100644 --- a/src/tools/base64-file-converter/index.ts +++ b/src/tools/base64-file-converter/index.ts @@ -4,7 +4,7 @@ import { defineTool } from '../tool'; export const tool = defineTool({ name: 'Base64 file converter', path: '/base64-file-converter', - description: "Convert string, files or images into a it's base64 representation.", + description: 'Convert string, files or images into a it\'s base64 representation.', keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'], component: () => import('./base64-file-converter.vue'), icon: FileDigit, diff --git a/src/tools/base64-string-converter/base64-string-converter.vue b/src/tools/base64-string-converter/base64-string-converter.vue index 9d574c9..486758c 100644 --- a/src/tools/base64-string-converter/base64-string-converter.vue +++ b/src/tools/base64-string-converter/base64-string-converter.vue @@ -1,3 +1,30 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import { useCopy } from '@/composable/copy'; +import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; +import { withDefaultOnError } from '@/utils/defaults'; + +const encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false); +const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false); + +const textInput = ref(''); +const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.value })); +const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' }); + +const base64Input = ref(''); +const textOutput = computed(() => + withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''), +); +const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); +const b64ValidationRules = [ + { + message: 'Invalid base64 string', + validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }), + }, +]; +const b64ValidationWatch = [decodeUrlSafe]; +</script> + <template> <c-card title="String to base64"> <n-form-item label="Encode URL safe" label-placement="left"> @@ -24,7 +51,9 @@ /> <div flex justify-center> - <c-button @click="copyTextBase64()"> Copy base64 </c-button> + <c-button @click="copyTextBase64()"> + Copy base64 + </c-button> </div> </c-card> @@ -54,34 +83,9 @@ /> <div flex justify-center> - <c-button @click="copyText()"> Copy decoded string </c-button> + <c-button @click="copyText()"> + Copy decoded string + </c-button> </div> </c-card> </template> - -<script setup lang="ts"> -import { useCopy } from '@/composable/copy'; -import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; -import { withDefaultOnError } from '@/utils/defaults'; -import { computed, ref } from 'vue'; - -const encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false); -const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false); - -const textInput = ref(''); -const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.value })); -const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' }); - -const base64Input = ref(''); -const textOutput = computed(() => - withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''), -); -const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); -const b64ValidationRules = [ - { - message: 'Invalid base64 string', - validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }), - }, -]; -const b64ValidationWatch = [decodeUrlSafe]; -</script> diff --git a/src/tools/basic-auth-generator/basic-auth-generator.vue b/src/tools/basic-auth-generator/basic-auth-generator.vue index 7e0660b..acca61f 100644 --- a/src/tools/basic-auth-generator/basic-auth-generator.vue +++ b/src/tools/basic-auth-generator/basic-auth-generator.vue @@ -1,3 +1,15 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import { useCopy } from '@/composable/copy'; +import { textToBase64 } from '@/utils/base64'; + +const username = ref(''); +const password = ref(''); +const header = computed(() => `Authorization: Basic ${textToBase64(`${username.value}:${password.value}`)}`); + +const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' }); +</script> + <template> <div> <c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 /> @@ -19,23 +31,13 @@ </n-statistic> </c-card> <div mt-5 flex justify-center> - <c-button @click="copy">Copy header</c-button> + <c-button @click="copy"> + Copy header + </c-button> </div> </div> </template> -<script setup lang="ts"> -import { useCopy } from '@/composable/copy'; -import { textToBase64 } from '@/utils/base64'; -import { computed, ref } from 'vue'; - -const username = ref(''); -const password = ref(''); -const header = computed(() => `Authorization: Basic ${textToBase64(`${username.value}:${password.value}`)}`); - -const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' }); -</script> - <style lang="less" scoped> ::v-deep(.n-statistic-value__content) { font-family: monospace; diff --git a/src/tools/bcrypt/bcrypt.vue b/src/tools/bcrypt/bcrypt.vue index eb366fa..893cc28 100644 --- a/src/tools/bcrypt/bcrypt.vue +++ b/src/tools/bcrypt/bcrypt.vue @@ -1,3 +1,21 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import { compareSync, hashSync } from 'bcryptjs'; +import { useThemeVars } from 'naive-ui'; +import { useCopy } from '@/composable/copy'; + +const themeVars = useThemeVars(); + +const input = ref(''); +const saltCount = ref(10); +const hashed = computed(() => hashSync(input.value, saltCount.value)); +const { copy } = useCopy({ source: hashed, text: 'Hashed string copied to the clipboard' }); + +const compareString = ref(''); +const compareHash = ref(''); +const compareMatch = computed(() => compareSync(compareString.value, compareHash.value)); +</script> + <template> <c-card title="Hash"> <c-input-text @@ -16,7 +34,9 @@ <c-input-text :value="hashed" readonly text-center /> <div mt-5 flex justify-center> - <c-button @click="copy"> Copy hash </c-button> + <c-button @click="copy"> + Copy hash + </c-button> </div> </c-card> @@ -37,24 +57,6 @@ </c-card> </template> -<script setup lang="ts"> -import { computed, ref } from 'vue'; -import { hashSync, compareSync } from 'bcryptjs'; -import { useCopy } from '@/composable/copy'; -import { useThemeVars } from 'naive-ui'; - -const themeVars = useThemeVars(); - -const input = ref(''); -const saltCount = ref(10); -const hashed = computed(() => hashSync(input.value, saltCount.value)); -const { copy } = useCopy({ source: hashed, text: 'Hashed string copied to the clipboard' }); - -const compareString = ref(''); -const compareHash = ref(''); -const compareMatch = computed(() => compareSync(compareString.value, compareHash.value)); -</script> - <style lang="less" scoped> .compare-result { color: v-bind('themeVars.errorColor'); diff --git a/src/tools/benchmark-builder/benchmark-builder.models.ts b/src/tools/benchmark-builder/benchmark-builder.models.ts index be8f965..10ae8a7 100644 --- a/src/tools/benchmark-builder/benchmark-builder.models.ts +++ b/src/tools/benchmark-builder/benchmark-builder.models.ts @@ -13,7 +13,7 @@ function computeAverage({ data }: { data: number[] }) { function computeVariance({ data }: { data: number[] }) { const mean = computeAverage({ data }); - const squaredDiffs = data.map((value) => Math.pow(value - mean, 2)); + const squaredDiffs = data.map(value => (value - mean) ** 2); return computeAverage({ data: squaredDiffs }); } @@ -24,11 +24,11 @@ function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; heade } const headers = Object.keys(data[0]); - const rows = data.map((obj) => Object.values(obj)); + const rows = data.map(obj => Object.values(obj)); - const headerRow = `| ${headers.map((header) => headerMap[header] ?? header).join(' | ')} |`; + const headerRow = `| ${headers.map(header => headerMap[header] ?? header).join(' | ')} |`; const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`; - const dataRows = rows.map((row) => `| ${row.join(' | ')} |`).join('\n'); + const dataRows = rows.map(row => `| ${row.join(' | ')} |`).join('\n'); return `${headerRow}\n${separatorRow}\n${dataRows}`; } diff --git a/src/tools/benchmark-builder/benchmark-builder.vue b/src/tools/benchmark-builder/benchmark-builder.vue index be350f2..d6642cc 100644 --- a/src/tools/benchmark-builder/benchmark-builder.vue +++ b/src/tools/benchmark-builder/benchmark-builder.vue @@ -1,89 +1,9 @@ -<template> - <n-scrollbar style="flex: 1" x-scrollable> - <div mb-5 flex flex-1 flex-nowrap justify-center gap-12px> - <div v-for="(suite, index) of suites" :key="index"> - <c-card style="width: 294px"> - <c-input-text - v-model:value="suite.title" - label-position="left" - label="Suite name" - placeholder="Suite name..." - clearable - /> - - <n-divider></n-divider> - <n-form-item label="Suite values" :show-feedback="false"> - <dynamic-values v-model:values="suite.data" /> - </n-form-item> - </c-card> - - <div flex justify-center> - <c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)"> - <n-icon :component="Trash" depth="3" mr-2 size="18" /> - Delete suite - </c-button> - <c-button - variant="text" - @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })" - > - <n-icon :component="Plus" depth="3" mr-2 size="18" /> - Add suite - </c-button> - </div> - </div> - </div> - </n-scrollbar> - - <div style="flex: 0 0 100%"> - <div style="max-width: 600px; margin: 0 auto"> - <div mx-auto max-w-sm flex justify-center gap-3> - <c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 /> - - <c-button - @click=" - suites = [ - { title: 'Suite 1', data: [] }, - { title: 'Suite 2', data: [] }, - ] - " - >Reset suites</c-button - > - </div> - - <n-table> - <thead> - <tr> - <th>{{ header.position }}</th> - <th>{{ header.title }}</th> - <th>{{ header.size }}</th> - <th>{{ header.mean }}</th> - <th>{{ header.variance }}</th> - </tr> - </thead> - <tbody> - <tr v-for="{ title, size, mean, variance, position } of results" :key="title"> - <td>{{ position }}</td> - <td>{{ title }}</td> - <td>{{ size }}</td> - <td>{{ mean }}</td> - <td>{{ variance }}</td> - </tr> - </tbody> - </n-table> - <div mt-5 flex justify-center gap-3> - <c-button @click="copyAsMarkdown">Copy as markdown table</c-button> - <c-button @click="copyAsBulletList">Copy as bullet list</c-button> - </div> - </div> - </div> -</template> - <script setup lang="ts"> -import { Trash, Plus } from '@vicons/tabler'; +import { Plus, Trash } from '@vicons/tabler'; import { useClipboard, useStorage } from '@vueuse/core'; import _ from 'lodash'; import { computed } from 'vue'; -import { computeAverage, computeVariance, arrayToMarkdownTable } from './benchmark-builder.models'; +import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models'; import DynamicValues from './dynamic-values.vue'; const suites = useStorage('benchmark-builder:suites', [ @@ -114,8 +34,8 @@ const results = computed(() => { const deltaWithBestMean = mean - bestMean; const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean); - const comparisonValues: string = - index !== 0 && bestMean !== mean ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : ''; + const comparisonValues: string + = (index !== 0 && bestMean !== mean) ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : ''; return { position: index + 1, @@ -157,4 +77,87 @@ function copyAsBulletList() { } </script> -<style lang="less" scoped></style> +<template> + <n-scrollbar style="flex: 1" x-scrollable> + <div mb-5 flex flex-1 flex-nowrap justify-center gap-12px> + <div v-for="(suite, index) of suites" :key="index"> + <c-card style="width: 294px"> + <c-input-text + v-model:value="suite.title" + label-position="left" + label="Suite name" + placeholder="Suite name..." + clearable + /> + + <n-divider /> + <n-form-item label="Suite values" :show-feedback="false"> + <DynamicValues v-model:values="suite.data" /> + </n-form-item> + </c-card> + + <div flex justify-center> + <c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)"> + <n-icon :component="Trash" depth="3" mr-2 size="18" /> + Delete suite + </c-button> + <c-button + variant="text" + @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })" + > + <n-icon :component="Plus" depth="3" mr-2 size="18" /> + Add suite + </c-button> + </div> + </div> + </div> + </n-scrollbar> + + <div style="flex: 0 0 100%"> + <div style="max-width: 600px; margin: 0 auto"> + <div mx-auto max-w-sm flex justify-center gap-3> + <c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 /> + + <c-button + @click=" + suites = [ + { title: 'Suite 1', data: [] }, + { title: 'Suite 2', data: [] }, + ] + " + > + Reset suites + </c-button> + </div> + + <n-table> + <thead> + <tr> + <th>{{ header.position }}</th> + <th>{{ header.title }}</th> + <th>{{ header.size }}</th> + <th>{{ header.mean }}</th> + <th>{{ header.variance }}</th> + </tr> + </thead> + <tbody> + <tr v-for="{ title, size, mean, variance, position } of results" :key="title"> + <td>{{ position }}</td> + <td>{{ title }}</td> + <td>{{ size }}</td> + <td>{{ mean }}</td> + <td>{{ variance }}</td> + </tr> + </tbody> + </n-table> + <div mt-5 flex justify-center gap-3> + <c-button @click="copyAsMarkdown"> + Copy as markdown table + </c-button> + <c-button @click="copyAsBulletList"> + Copy as bullet list + </c-button> + </div> + </div> + </div> +</template> diff --git a/src/tools/benchmark-builder/dynamic-values.vue b/src/tools/benchmark-builder/dynamic-values.vue index 5e349fc..975a545 100644 --- a/src/tools/benchmark-builder/dynamic-values.vue +++ b/src/tools/benchmark-builder/dynamic-values.vue @@ -1,7 +1,37 @@ +<script setup lang="ts"> +import { Plus, Trash } from '@vicons/tabler'; +import { useTemplateRefsList, useVModel } from '@vueuse/core'; +import { NInputNumber } from 'naive-ui'; +import { nextTick } from 'vue'; + +const props = defineProps<{ values: (number | null)[] }>(); + +const emit = defineEmits(['update:values']); + +const refs = useTemplateRefsList<typeof NInputNumber>(); + +const values = useVModel(props, 'values', emit); + +async function addValue() { + values.value.push(null); + await nextTick(); + refs.value.at(-1)?.focus(); +} + +function onInputEnter(index: number) { + if (index === values.value.length - 1) { + addValue(); + return; + } + + refs.value.at(index + 1)?.focus(); +} +</script> + <template> <div> <div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2> - <n-input-number + <NInputNumber :ref="refs.set" v-model:value="values[index]" :show-button="false" @@ -25,33 +55,3 @@ </c-button> </div> </template> - -<script setup lang="ts"> -import { Trash, Plus } from '@vicons/tabler'; -import { useTemplateRefsList, useVModel } from '@vueuse/core'; -import { NInputNumber } from 'naive-ui'; -import { nextTick } from 'vue'; - -const refs = useTemplateRefsList<typeof NInputNumber>(); - -const props = defineProps<{ values: (number | null)[] }>(); -const emit = defineEmits(['update:values']); -const values = useVModel(props, 'values', emit); - -async function addValue() { - values.value.push(null); - await nextTick(); - refs.value.at(-1)?.focus(); -} - -function onInputEnter(index: number) { - if (index === values.value.length - 1) { - addValue(); - return; - } - - refs.value.at(index + 1)?.focus(); -} -</script> - -<style scoped></style> diff --git a/src/tools/bip39-generator/bip39-generator.vue b/src/tools/bip39-generator/bip39-generator.vue index 1aaa6e9..5f005ed 100644 --- a/src/tools/bip39-generator/bip39-generator.vue +++ b/src/tools/bip39-generator/bip39-generator.vue @@ -1,58 +1,4 @@ -<template> - <div> - <n-grid cols="3" x-gap="12"> - <n-gi span="1"> - <n-form-item label="Language:"> - <n-select - v-model:value="language" - :options="Object.keys(languages).map((label) => ({ label, value: label }))" - /> - </n-form-item> - </n-gi> - <n-gi span="2"> - <n-form-item - label="Entropy (seed):" - :feedback="entropyValidation.message" - :validation-status="entropyValidation.status" - > - <n-input-group> - <c-input-text v-model:value="entropy" placeholder="Your string..." /> - - <c-button @click="refreshEntropy"> - <n-icon size="22"> - <Refresh /> - </n-icon> - </c-button> - <c-button @click="copyEntropy"> - <n-icon size="22"> - <Copy /> - </n-icon> - </c-button> - </n-input-group> - </n-form-item> - </n-gi> - </n-grid> - <n-form-item - label="Passphrase (mnemonic):" - :feedback="mnemonicValidation.message" - :validation-status="mnemonicValidation.status" - > - <n-input-group> - <c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text /> - - <c-button @click="copyPassphrase"> - <n-icon size="22" :component="Copy" /> - </c-button> - </n-input-group> - </n-form-item> - </div> -</template> - <script setup lang="ts"> -import { useCopy } from '@/composable/copy'; -import { useValidation } from '@/composable/validation'; -import { isNotThrowing } from '@/utils/boolean'; -import { withDefaultOnError } from '@/utils/defaults'; import { chineseSimplifiedWordList, chineseTraditionalWordList, @@ -70,18 +16,22 @@ import { } from '@it-tools/bip39'; import { Copy, Refresh } from '@vicons/tabler'; import { computed, ref } from 'vue'; +import { useCopy } from '@/composable/copy'; +import { useValidation } from '@/composable/validation'; +import { isNotThrowing } from '@/utils/boolean'; +import { withDefaultOnError } from '@/utils/defaults'; const languages = { - English: englishWordList, + 'English': englishWordList, 'Chinese simplified': chineseSimplifiedWordList, 'Chinese traditional': chineseTraditionalWordList, - Czech: czechWordList, - French: frenchWordList, - Italian: italianWordList, - Japanese: japaneseWordList, - Korean: koreanWordList, - Portuguese: portugueseWordList, - Spanish: spanishWordList, + 'Czech': czechWordList, + 'French': frenchWordList, + 'Italian': italianWordList, + 'Japanese': japaneseWordList, + 'Korean': koreanWordList, + 'Portuguese': portugueseWordList, + 'Spanish': spanishWordList, }; const entropy = ref(generateEntropy()); @@ -102,11 +52,11 @@ const entropyValidation = useValidation({ source: entropy, rules: [ { - validator: (value) => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0), + validator: value => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0), message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4', }, { - validator: (value) => /^[a-fA-F0-9]*$/.test(value), + validator: value => /^[a-fA-F0-9]*$/.test(value), message: 'Entropy should be an hexadecimal string', }, ], @@ -116,7 +66,7 @@ const mnemonicValidation = useValidation({ source: passphrase, rules: [ { - validator: (value) => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])), + validator: value => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])), message: 'Invalid mnemonic', }, ], @@ -129,3 +79,53 @@ function refreshEntropy() { const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' }); const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' }); </script> + +<template> + <div> + <n-grid cols="3" x-gap="12"> + <n-gi span="1"> + <n-form-item label="Language:"> + <n-select + v-model:value="language" + :options="Object.keys(languages).map((label) => ({ label, value: label }))" + /> + </n-form-item> + </n-gi> + <n-gi span="2"> + <n-form-item + label="Entropy (seed):" + :feedback="entropyValidation.message" + :validation-status="entropyValidation.status" + > + <n-input-group> + <c-input-text v-model:value="entropy" placeholder="Your string..." /> + + <c-button @click="refreshEntropy"> + <n-icon size="22"> + <Refresh /> + </n-icon> + </c-button> + <c-button @click="copyEntropy"> + <n-icon size="22"> + <Copy /> + </n-icon> + </c-button> + </n-input-group> + </n-form-item> + </n-gi> + </n-grid> + <n-form-item + label="Passphrase (mnemonic):" + :feedback="mnemonicValidation.message" + :validation-status="mnemonicValidation.status" + > + <n-input-group> + <c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text /> + + <c-button @click="copyPassphrase"> + <n-icon size="22" :component="Copy" /> + </c-button> + </n-input-group> + </n-form-item> + </div> +</template> diff --git a/src/tools/camera-recorder/camera-recorder.vue b/src/tools/camera-recorder/camera-recorder.vue index 81fec42..19fe30b 100644 --- a/src/tools/camera-recorder/camera-recorder.vue +++ b/src/tools/camera-recorder/camera-recorder.vue @@ -1,6 +1,110 @@ +<script setup lang="ts"> +import _ from 'lodash'; + +import { useMediaRecorder } from './useMediaRecorder'; + +interface Media { type: 'image' | 'video'; value: string; createdAt: Date } + +const { + videoInputs: cameras, + audioInputs: microphones, + permissionGranted, + isSupported, + ensurePermissions, +} = useDevicesList({ + requestPermissions: true, + constraints: { video: true, audio: true }, + onUpdated() { + refreshCurrentDevices(); + }, +}); + +const video = ref<HTMLVideoElement>(); +const medias = ref<Media[]>([]); +const currentCamera = ref(cameras.value[0]?.deviceId); +const currentMicrophone = ref(microphones.value[0]?.deviceId); +const permissionCannotBePrompted = ref(false); + +const { + stream, + start, + enabled: isMediaStreamAvailable, +} = useUserMedia({ + constraints: computed(() => ({ + video: { deviceId: currentCamera.value }, + ...(currentMicrophone.value ? { audio: { deviceId: currentMicrophone.value } } : {}), + })), + autoSwitch: true, +}); + +const { + isRecordingSupported, + onRecordAvailable, + startRecording, + stopRecording, + pauseRecording, + recordingState, + resumeRecording, +} = useMediaRecorder({ + stream, +}); + +onRecordAvailable((value) => { + medias.value.unshift({ type: 'video', value, createdAt: new Date() }); +}); + +function refreshCurrentDevices() { + if (_.isNil(currentCamera) || !cameras.value.find(i => i.deviceId === currentCamera.value)) { + currentCamera.value = cameras.value[0]?.deviceId; + } + + if (_.isNil(microphones) || !microphones.value.find(i => i.deviceId === currentMicrophone.value)) { + currentMicrophone.value = microphones.value[0]?.deviceId; + } +} + +function takeScreenshot() { + if (!video.value) { + return; + } + + const canvas = document.createElement('canvas'); + canvas.width = video.value.videoWidth; + canvas.height = video.value.videoHeight; + canvas.getContext('2d')?.drawImage(video.value, 0, 0); + const image = canvas.toDataURL('image/png'); + + medias.value.unshift({ type: 'image', value: image, createdAt: new Date() }); +} + +watchEffect(() => { + if (video.value && stream.value) { + video.value.srcObject = stream.value; + } +}); + +async function requestPermissions() { + try { + await ensurePermissions(); + } + catch (e) { + permissionCannotBePrompted.value = true; + } +} + +function downloadMedia({ type, value, createdAt }: Media) { + const link = document.createElement('a'); + link.href = value; + link.download = `${type}-${createdAt.getTime()}.${type === 'image' ? 'png' : 'webm'}`; + link.click(); +} +</script> + <template> <div> - <c-card v-if="!isSupported"> Your browser does not support recording video from camera </c-card> + <c-card v-if="!isSupported"> + Your browser does not support recording video from camera + </c-card> <c-card v-else-if="!permissionGranted" text-center> You need to grant permission to use your camera and microphone @@ -11,7 +115,9 @@ </c-alert> <div v-else mt-4 flex justify-center> - <c-button @click="requestPermissions">Grant permission</c-button> + <c-button @click="requestPermissions"> + Grant permission + </c-button> </div> </c-card> @@ -36,7 +142,9 @@ </div> <div v-if="!isMediaStreamAvailable" mt-3 flex justify-center> - <c-button type="primary" @click="start">Start webcam</c-button> + <c-button type="primary" @click="start"> + Start webcam + </c-button> </div> <div v-else> @@ -71,19 +179,23 @@ Stop </c-button> </div> - <div v-else italic op-60>Video recording is not supported in your browser</div> + <div v-else italic op-60> + Video recording is not supported in your browser + </div> </div> </div> </c-card> <div grid grid-cols-2 mt-5 gap-2> <c-card v-for="({ type, value, createdAt }, index) in medias" :key="index"> - <img v-if="type === 'image'" :src="value" max-h-full w-full alt="screenshot" /> + <img v-if="type === 'image'" :src="value" max-h-full w-full alt="screenshot"> <video v-else :src="value" controls max-h-full w-full /> <div flex items-center justify-between> - <div font-bold>{{ type === 'image' ? 'Screenshot' : 'Video' }}</div> + <div font-bold> + {{ type === 'image' ? 'Screenshot' : 'Video' }} + </div> <div flex gap-2> <c-button @click="downloadMedia({ type, value, createdAt })"> @@ -99,104 +211,3 @@ </div> </div> </template> - -<script setup lang="ts"> -import _ from 'lodash'; - -import { useMediaRecorder } from './useMediaRecorder'; - -type Media = { type: 'image' | 'video'; value: string; createdAt: Date }; - -const { - videoInputs: cameras, - audioInputs: microphones, - permissionGranted, - isSupported, - ensurePermissions, -} = useDevicesList({ - requestPermissions: true, - constraints: { video: true, audio: true }, - onUpdated() { - refreshCurrentDevices(); - }, -}); - -const video = ref<HTMLVideoElement>(); -const medias = ref<Media[]>([]); -const currentCamera = ref(cameras.value[0]?.deviceId); -const currentMicrophone = ref(microphones.value[0]?.deviceId); -const permissionCannotBePrompted = ref(false); - -const { - stream, - start, - enabled: isMediaStreamAvailable, -} = useUserMedia({ - constraints: computed(() => ({ - video: { deviceId: currentCamera.value }, - ...(currentMicrophone.value ? { audio: { deviceId: currentMicrophone.value } } : {}), - })), - autoSwitch: true, -}); - -const { - isRecordingSupported, - onRecordAvailable, - startRecording, - stopRecording, - pauseRecording, - recordingState, - resumeRecording, -} = useMediaRecorder({ - stream, -}); - -onRecordAvailable((value) => { - medias.value.unshift({ type: 'video', value, createdAt: new Date() }); -}); - -function refreshCurrentDevices() { - console.log('refreshCurrentDevices'); - - if (_.isNil(currentCamera) || !cameras.value.find((i) => i.deviceId === currentCamera.value)) { - currentCamera.value = cameras.value[0]?.deviceId; - } - - if (_.isNil(microphones) || !microphones.value.find((i) => i.deviceId === currentMicrophone.value)) { - currentMicrophone.value = microphones.value[0]?.deviceId; - } -} - -function takeScreenshot() { - if (!video.value) return; - - const canvas = document.createElement('canvas'); - canvas.width = video.value.videoWidth; - canvas.height = video.value.videoHeight; - canvas.getContext('2d')?.drawImage(video.value, 0, 0); - const image = canvas.toDataURL('image/png'); - - medias.value.unshift({ type: 'image', value: image, createdAt: new Date() }); -} - -watchEffect(() => { - if (video.value && stream.value) video.value.srcObject = stream.value; -}); - -async function requestPermissions() { - try { - await ensurePermissions(); - } catch (e) { - permissionCannotBePrompted.value = true; - } -} - -function downloadMedia({ type, value, createdAt }: Media) { - const link = document.createElement('a'); - link.href = value; - link.download = `${type}-${createdAt.getTime()}.${type === 'image' ? 'png' : 'webm'}`; - link.click(); -} -</script> - -<style lang="less" scoped></style> diff --git a/src/tools/camera-recorder/useMediaRecorder.ts b/src/tools/camera-recorder/useMediaRecorder.ts index eed0edd..a21328c 100644 --- a/src/tools/camera-recorder/useMediaRecorder.ts +++ b/src/tools/camera-recorder/useMediaRecorder.ts @@ -1,15 +1,15 @@ -import { computed, ref, type Ref } from 'vue'; +import { type Ref, computed, ref } from 'vue'; export { useMediaRecorder }; function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): { - isRecordingSupported: Ref<boolean>; - recordingState: Ref<'stopped' | 'recording' | 'paused'>; - startRecording: () => void; - stopRecording: () => void; - pauseRecording: () => void; - resumeRecording: () => void; - onRecordAvailable: (cb: (url: string) => void) => void; + isRecordingSupported: Ref<boolean> + recordingState: Ref<'stopped' | 'recording' | 'paused'> + startRecording: () => void + stopRecording: () => void + pauseRecording: () => void + resumeRecording: () => void + onRecordAvailable: (cb: (url: string) => void) => void } { const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm')); const mediaRecorder = ref<MediaRecorder | null>(null); @@ -17,10 +17,23 @@ function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): const recordAvailable = createEventHook(); const recordingState = ref<'stopped' | 'recording' | 'paused'>('stopped'); + const createVideo = () => { + const blob = new Blob(recordedChunks.value, { type: 'video/webm' }); + const url = URL.createObjectURL(blob); + recordedChunks.value = []; + return url; + }; + const startRecording = () => { - if (!isRecordingSupported.value) return; - if (!stream.value) return; - if (recordingState.value !== 'stopped') return; + if (!isRecordingSupported.value) { + return; + } + if (!stream.value) { + return; + } + if (recordingState.value !== 'stopped') { + return; + } mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' }); @@ -34,47 +47,60 @@ function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): recordAvailable.trigger(createVideo()); }; - if (mediaRecorder.value.state !== 'inactive') return; + if (mediaRecorder.value.state !== 'inactive') { + return; + } mediaRecorder.value.start(); recordingState.value = 'recording'; }; const stopRecording = () => { - if (!isRecordingSupported.value) return; - if (!mediaRecorder.value) return; - if (recordingState.value === 'stopped') return; + if (!isRecordingSupported.value) { + return; + } + if (!mediaRecorder.value) { + return; + } + if (recordingState.value === 'stopped') { + return; + } mediaRecorder.value.stop(); recordingState.value = 'stopped'; }; const pauseRecording = () => { - if (!isRecordingSupported.value) return; - if (!mediaRecorder.value) return; - if (recordingState.value !== 'recording') return; + if (!isRecordingSupported.value) { + return; + } + if (!mediaRecorder.value) { + return; + } + if (recordingState.value !== 'recording') { + return; + } mediaRecorder.value.pause(); recordingState.value = 'paused'; }; const resumeRecording = () => { - if (!isRecordingSupported.value) return; - if (!mediaRecorder.value) return; + if (!isRecordingSupported.value) { + return; + } + if (!mediaRecorder.value) { + return; + } - if (recordingState.value !== 'paused') return; + if (recordingState.value !== 'paused') { + return; + } mediaRecorder.value.resume(); recordingState.value = 'recording'; }; - const createVideo = () => { - const blob = new Blob(recordedChunks.value, { type: 'video/webm' }); - const url = URL.createObjectURL(blob); - recordedChunks.value = []; - return url; - }; - return { isRecordingSupported, startRecording, diff --git a/src/tools/case-converter/case-converter.vue b/src/tools/case-converter/case-converter.vue index 2ad3656..4cf6507 100644 --- a/src/tools/case-converter/case-converter.vue +++ b/src/tools/case-converter/case-converter.vue @@ -1,3 +1,27 @@ +<script setup lang="ts"> +import { ref } from 'vue'; +import { + camelCase, + capitalCase, + constantCase, + dotCase, + headerCase, + noCase, + paramCase, + pascalCase, + pathCase, + sentenceCase, + snakeCase, +} from 'change-case'; +import InputCopyable from '../../components/InputCopyable.vue'; + +const baseConfig = { + stripRegexp: /[^A-Za-zÀ-ÖØ-öø-ÿ]+/gi, +}; + +const input = ref('lorem ipsum dolor sit amet'); +</script> + <template> <c-card> <n-form label-width="120" label-placement="left" :show-feedback="false"> @@ -14,66 +38,42 @@ <n-divider /> <n-form-item label="Camelcase:"> - <input-copyable :value="camelCase(input, baseConfig)" /> + <InputCopyable :value="camelCase(input, baseConfig)" /> </n-form-item> <n-form-item label="Capitalcase:"> - <input-copyable :value="capitalCase(input, baseConfig)" /> + <InputCopyable :value="capitalCase(input, baseConfig)" /> </n-form-item> <n-form-item label="Constantcase:"> - <input-copyable :value="constantCase(input, baseConfig)" /> + <InputCopyable :value="constantCase(input, baseConfig)" /> </n-form-item> <n-form-item label="Dotcase:"> - <input-copyable :value="dotCase(input, baseConfig)" /> + <InputCopyable :value="dotCase(input, baseConfig)" /> </n-form-item> <n-form-item label="Headercase:"> - <input-copyable :value="headerCase(input, baseConfig)" /> + <InputCopyable :value="headerCase(input, baseConfig)" /> </n-form-item> <n-form-item label="Nocase:"> - <input-copyable :value="noCase(input, baseConfig)" /> + <InputCopyable :value="noCase(input, baseConfig)" /> </n-form-item> <n-form-item label="Paramcase:"> - <input-copyable :value="paramCase(input, baseConfig)" /> + <InputCopyable :value="paramCase(input, baseConfig)" /> </n-form-item> <n-form-item label="Pascalcase:"> - <input-copyable :value="pascalCase(input, baseConfig)" /> + <InputCopyable :value="pascalCase(input, baseConfig)" /> </n-form-item> <n-form-item label="Pathcase:"> - <input-copyable :value="pathCase(input, baseConfig)" /> + <InputCopyable :value="pathCase(input, baseConfig)" /> </n-form-item> <n-form-item label="Sentencecase:"> - <input-copyable :value="sentenceCase(input, baseConfig)" /> + <InputCopyable :value="sentenceCase(input, baseConfig)" /> </n-form-item> <n-form-item label="Snakecase:"> - <input-copyable :value="snakeCase(input, baseConfig)" /> + <InputCopyable :value="snakeCase(input, baseConfig)" /> </n-form-item> </n-form> </c-card> </template> -<script setup lang="ts"> -import { ref } from 'vue'; -import { - camelCase, - capitalCase, - constantCase, - dotCase, - headerCase, - noCase, - paramCase, - pascalCase, - pathCase, - sentenceCase, - snakeCase, -} from 'change-case'; -import InputCopyable from '../../components/InputCopyable.vue'; - -const baseConfig = { - stripRegexp: /[^A-Za-zÀ-ÖØ-öø-ÿ]+/gi, -}; - -const input = ref('lorem ipsum dolor sit amet'); -</script> - <style lang="less" scoped> .n-form-item { margin: 5px 0; diff --git a/src/tools/chmod-calculator/chmod-calculator.service.test.ts b/src/tools/chmod-calculator/chmod-calculator.service.test.ts index fafb393..ff09fa6 100644 --- a/src/tools/chmod-calculator/chmod-calculator.service.test.ts +++ b/src/tools/chmod-calculator/chmod-calculator.service.test.ts @@ -1,4 +1,4 @@ -import { expect, describe, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { computeChmodOctalRepresentation } from './chmod-calculator.service'; describe('chmod-calculator', () => { diff --git a/src/tools/chmod-calculator/chmod-calculator.vue b/src/tools/chmod-calculator/chmod-calculator.vue index d37f95f..86f36da 100644 --- a/src/tools/chmod-calculator/chmod-calculator.vue +++ b/src/tools/chmod-calculator/chmod-calculator.vue @@ -1,38 +1,8 @@ -<template> - <div> - <n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table"> - <thead> - <tr> - <th class="text-center" scope="col"></th> - <th class="text-center" scope="col">Owner (u)</th> - <th class="text-center" scope="col">Group (g)</th> - <th class="text-center" scope="col">Public (o)</th> - </tr> - </thead> - <tbody> - <tr v-for="{ scope, title } of scopes" :key="scope"> - <td class="line-header">{{ title }}</td> - <td v-for="group of groups" :key="group" class="text-center"> - <!-- <n-switch v-model:value="permissions[group][scope]" /> --> - <n-checkbox v-model:checked="permissions[group][scope]" size="large" /> - </td> - </tr> - </tbody> - </n-table> - - <div class="octal-result"> - {{ octal }} - </div> - - <input-copyable :value="`chmod ${octal} path`" readonly /> - </div> -</template> - <script setup lang="ts"> import { useThemeVars } from 'naive-ui'; import { computed, ref } from 'vue'; -import { computeChmodOctalRepresentation } from './chmod-calculator.service'; import InputCopyable from '../../components/InputCopyable.vue'; +import { computeChmodOctalRepresentation } from './chmod-calculator.service'; import type { Group, Scope } from './chmod-calculator.types'; @@ -54,6 +24,44 @@ const permissions = ref({ const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value })); </script> +<template> + <div> + <n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table"> + <thead> + <tr> + <th class="text-center" scope="col" /> + <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> + + <InputCopyable :value="`chmod ${octal} path`" readonly /> + </div> +</template> + <style lang="less" scoped> .octal-result { text-align: center; diff --git a/src/tools/chronometer/chronometer.vue b/src/tools/chronometer/chronometer.vue index bcd01a2..30a533e 100644 --- a/src/tools/chronometer/chronometer.vue +++ b/src/tools/chronometer/chronometer.vue @@ -1,17 +1,3 @@ -<template> - <div> - <c-card> - <div class="duration">{{ formatMs(counter) }}</div> - </c-card> - <div mt-5 flex justify-center gap-3> - <c-button v-if="!isRunning" type="primary" @click="resume">Start</c-button> - <c-button v-else type="warning" @click="pause">Stop</c-button> - - <c-button @click="counter = 0">Reset</c-button> - </div> - </div> -</template> - <script setup lang="ts"> import { useRafFn } from '@vueuse/core'; import { ref } from 'vue'; @@ -42,6 +28,28 @@ function pause() { } </script> +<template> + <div> + <c-card> + <div class="duration"> + {{ formatMs(counter) }} + </div> + </c-card> + <div mt-5 flex justify-center gap-3> + <c-button v-if="!isRunning" type="primary" @click="resume"> + Start + </c-button> + <c-button v-else type="warning" @click="pause"> + Stop + </c-button> + + <c-button @click="counter = 0"> + Reset + </c-button> + </div> + </div> +</template> + <style lang="less" scoped> .duration { text-align: center; diff --git a/src/tools/color-converter/color-converter.vue b/src/tools/color-converter/color-converter.vue index bfd4912..0513a12 100644 --- a/src/tools/color-converter/color-converter.vue +++ b/src/tools/color-converter/color-converter.vue @@ -1,38 +1,3 @@ -<template> - <c-card> - <n-form label-width="100" label-placement="left"> - <n-form-item label="color picker:"> - <n-color-picker - v-model:value="hex" - placement="bottom-end" - @update:value="(v: string) => onInputUpdated(v, 'hex')" - /> - </n-form-item> - <n-form-item label="color name:"> - <input-copyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" /> - </n-form-item> - <n-form-item label="hex:"> - <input-copyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" /> - </n-form-item> - <n-form-item label="rgb:"> - <input-copyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" /> - </n-form-item> - <n-form-item label="hsl:"> - <input-copyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" /> - </n-form-item> - <n-form-item label="hwb:"> - <input-copyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" /> - </n-form-item> - <n-form-item label="lch:"> - <input-copyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" /> - </n-form-item> - <n-form-item label="cmyk:"> - <input-copyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" /> - </n-form-item> - </n-form> - </c-card> -</template> - <script setup lang="ts"> import { ref } from 'vue'; import { colord, extend } from 'colord'; @@ -57,17 +22,67 @@ function onInputUpdated(value: string, omit: string) { try { const color = colord(value); - if (omit !== 'name') name.value = color.toName({ closest: true }) ?? ''; - if (omit !== 'hex') hex.value = color.toHex(); - if (omit !== 'rgb') rgb.value = color.toRgbString(); - if (omit !== 'hsl') hsl.value = color.toHslString(); - if (omit !== 'hwb') hwb.value = color.toHwbString(); - if (omit !== 'cmyk') cmyk.value = color.toCmykString(); - if (omit !== 'lch') lch.value = color.toLchString(); - } catch { + if (omit !== 'name') { + name.value = color.toName({ closest: true }) ?? ''; + } + if (omit !== 'hex') { + hex.value = color.toHex(); + } + if (omit !== 'rgb') { + rgb.value = color.toRgbString(); + } + if (omit !== 'hsl') { + hsl.value = color.toHslString(); + } + if (omit !== 'hwb') { + hwb.value = color.toHwbString(); + } + if (omit !== 'cmyk') { + cmyk.value = color.toCmykString(); + } + if (omit !== 'lch') { + lch.value = color.toLchString(); + } + } + catch { // } } onInputUpdated(hex.value, 'hex'); </script> + +<template> + <c-card> + <n-form label-width="100" label-placement="left"> + <n-form-item label="color picker:"> + <n-color-picker + v-model:value="hex" + placement="bottom-end" + @update:value="(v: string) => onInputUpdated(v, 'hex')" + /> + </n-form-item> + <n-form-item label="color name:"> + <InputCopyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" /> + </n-form-item> + <n-form-item label="hex:"> + <InputCopyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" /> + </n-form-item> + <n-form-item label="rgb:"> + <InputCopyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" /> + </n-form-item> + <n-form-item label="hsl:"> + <InputCopyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" /> + </n-form-item> + <n-form-item label="hwb:"> + <InputCopyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" /> + </n-form-item> + <n-form-item label="lch:"> + <InputCopyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" /> + </n-form-item> + <n-form-item label="cmyk:"> + <InputCopyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" /> + </n-form-item> + </n-form> + </c-card> +</template> diff --git a/src/tools/crontab-generator/crontab-generator.vue b/src/tools/crontab-generator/crontab-generator.vue index d2559d7..5f02c93 100644 --- a/src/tools/crontab-generator/crontab-generator.vue +++ b/src/tools/crontab-generator/crontab-generator.vue @@ -1,89 +1,3 @@ -<template> - <c-card> - <div mx-auto max-w-sm> - <c-input-text - v-model:value="cron" - size="large" - placeholder="* * * * *" - :validation-rules="cronValidationRules" - mb-3 - /> - </div> - - <div class="cron-string"> - {{ cronString }} - </div> - - <n-divider /> - - <div flex justify-center> - <n-form :show-feedback="false" label-width="170" label-placement="left"> - <n-form-item label="Verbose"> - <n-switch v-model:value="cronstrueConfig.verbose" /> - </n-form-item> - <n-form-item label="Use 24 hour time format"> - <n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" /> - </n-form-item> - <n-form-item label="Days start at 0"> - <n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" /> - </n-form-item> - </n-form> - </div> - </c-card> - <c-card> - <pre> -┌──────────── [optional] seconds (0 - 59) -| ┌────────── minute (0 - 59) -| | ┌──────── hour (0 - 23) -| | | ┌────── day of month (1 - 31) -| | | | ┌──── month (1 - 12) OR jan,feb,mar,apr ... -| | | | | ┌── day of week (0 - 6, sunday=0) OR sun,mon ... -| | | | | | -* * * * * * command</pre - > - - <div v-if="styleStore.isSmallScreen"> - <c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none> - <div> - Symbol: <strong>{{ symbol }}</strong> - </div> - <div> - Meaning: <strong>{{ meaning }}</strong> - </div> - <div> - Example: - <strong - ><code>{{ example }}</code></strong - > - </div> - <div> - Equivalent: <strong>{{ equivalent }}</strong> - </div> - </c-card> - </div> - <n-table v-else size="small"> - <thead> - <tr> - <th class="text-left" scope="col">Symbol</th> - <th class="text-left" scope="col">Meaning</th> - <th class="text-left" scope="col">Example</th> - <th class="text-left" scope="col">Equivalent</th> - </tr> - </thead> - <tbody> - <tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol"> - <td>{{ symbol }}</td> - <td>{{ meaning }}</td> - <td> - <code>{{ example }}</code> - </td> - <td>{{ equivalent }}</td> - </tr> - </tbody> - </n-table> - </c-card> -</template> - <script setup lang="ts"> import cronstrue from 'cronstrue'; import { isValidCron } from 'cron-validator'; @@ -194,6 +108,97 @@ const cronValidationRules = [ ]; </script> +<template> + <c-card> + <div mx-auto max-w-sm> + <c-input-text + v-model:value="cron" + size="large" + placeholder="* * * * *" + :validation-rules="cronValidationRules" + mb-3 + /> + </div> + + <div class="cron-string"> + {{ cronString }} + </div> + + <n-divider /> + + <div flex justify-center> + <n-form :show-feedback="false" label-width="170" label-placement="left"> + <n-form-item label="Verbose"> + <n-switch v-model:value="cronstrueConfig.verbose" /> + </n-form-item> + <n-form-item label="Use 24 hour time format"> + <n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" /> + </n-form-item> + <n-form-item label="Days start at 0"> + <n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" /> + </n-form-item> + </n-form> + </div> + </c-card> + <c-card> + <pre> +┌──────────── [optional] seconds (0 - 59) +| ┌────────── minute (0 - 59) +| | ┌──────── hour (0 - 23) +| | | ┌────── day of month (1 - 31) +| | | | ┌──── month (1 - 12) OR jan,feb,mar,apr ... +| | | | | ┌── day of week (0 - 6, sunday=0) OR sun,mon ... +| | | | | | +* * * * * * command</pre> + + <div v-if="styleStore.isSmallScreen"> + <c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none> + <div> + Symbol: <strong>{{ symbol }}</strong> + </div> + <div> + Meaning: <strong>{{ meaning }}</strong> + </div> + <div> + Example: + <strong><code>{{ example }}</code></strong> + </div> + <div> + Equivalent: <strong>{{ equivalent }}</strong> + </div> + </c-card> + </div> + <n-table v-else size="small"> + <thead> + <tr> + <th class="text-left" scope="col"> + Symbol + </th> + <th class="text-left" scope="col"> + Meaning + </th> + <th class="text-left" scope="col"> + Example + </th> + <th class="text-left" scope="col"> + Equivalent + </th> + </tr> + </thead> + <tbody> + <tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol"> + <td>{{ symbol }}</td> + <td>{{ meaning }}</td> + <td> + <code>{{ example }}</code> + </td> + <td>{{ equivalent }}</td> + </tr> + </tbody> + </n-table> + </c-card> +</template> + <style lang="less" scoped> ::v-deep(input) { font-size: 30px; diff --git a/src/tools/date-time-converter/date-time-converter.e2e.spec.ts b/src/tools/date-time-converter/date-time-converter.e2e.spec.ts index 5015560..34ee749 100644 --- a/src/tools/date-time-converter/date-time-converter.e2e.spec.ts +++ b/src/tools/date-time-converter/date-time-converter.e2e.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.describe('Date time converter - json to yaml', () => { test.beforeEach(async ({ page }) => { diff --git a/src/tools/date-time-converter/date-time-converter.models.test.ts b/src/tools/date-time-converter/date-time-converter.models.test.ts index e3a82f8..502cdc6 100644 --- a/src/tools/date-time-converter/date-time-converter.models.test.ts +++ b/src/tools/date-time-converter/date-time-converter.models.test.ts @@ -1,13 +1,13 @@ -import { describe, test, expect } from 'vitest'; +import { describe, expect, test } from 'vitest'; import { isISO8601DateTimeString, isISO9075DateString, + isMongoObjectId, isRFC3339DateString, isRFC7231DateString, - isUnixTimestamp, isTimestamp, isUTCDateString, - isMongoObjectId, + isUnixTimestamp, } from './date-time-converter.models'; describe('date-time-converter models', () => { diff --git a/src/tools/date-time-converter/date-time-converter.models.ts b/src/tools/date-time-converter/date-time-converter.models.ts index 3697c16..173b8a8 100644 --- a/src/tools/date-time-converter/date-time-converter.models.ts +++ b/src/tools/date-time-converter/date-time-converter.models.ts @@ -11,13 +11,13 @@ export { isMongoObjectId, }; -const ISO8601_REGEX = - /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; -const ISO9075_REGEX = - /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/; +const ISO8601_REGEX + = /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; +const ISO9075_REGEX + = /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/; -const RFC3339_REGEX = - /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/; +const RFC3339_REGEX + = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/; const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/; @@ -40,7 +40,8 @@ function isUTCDateString(date?: string) { try { return new Date(date).toUTCString() === date; - } catch (_ignored) { + } + catch (_ignored) { return false; } } diff --git a/src/tools/date-time-converter/date-time-converter.types.ts b/src/tools/date-time-converter/date-time-converter.types.ts index 7c78b11..669c92a 100644 --- a/src/tools/date-time-converter/date-time-converter.types.ts +++ b/src/tools/date-time-converter/date-time-converter.types.ts @@ -1,8 +1,8 @@ export type ToDateMapper = (value: string) => Date; -export type DateFormat = { - name: string; - fromDate: (date: Date) => string; - toDate: (value: string) => Date; - formatMatcher: (dateString: string) => boolean; -}; +export interface DateFormat { + name: string + fromDate: (date: Date) => string + toDate: (value: string) => Date + formatMatcher: (dateString: string) => boolean +} diff --git a/src/tools/date-time-converter/date-time-converter.vue b/src/tools/date-time-converter/date-time-converter.vue index 4207eb9..fcebce8 100644 --- a/src/tools/date-time-converter/date-time-converter.vue +++ b/src/tools/date-time-converter/date-time-converter.vue @@ -1,42 +1,3 @@ -<template> - <div> - <n-input-group> - <c-input-text - v-model:value="inputDate" - autofocus - placeholder="Put you date string here..." - clearable - test-id="date-time-converter-input" - :validation="validation" - @update:value="onDateInputChanged" - /> - - <n-select - v-model:value="formatIndex" - style="flex: 0 0 170px" - :options="formats.map(({ name }, i) => ({ label: name, value: i }))" - data-test-id="date-time-converter-format-select" - /> - </n-input-group> - - <n-divider /> - - <input-copyable - v-for="{ name, fromDate } in formats" - :key="name" - :label="name" - label-width="150px" - label-position="left" - label-align="right" - :value="formatDateUsingFormatter(fromDate, normalizedDate)" - placeholder="Invalid date..." - :test-id="name" - readonly - mt-2 - /> - </div> -</template> - <script setup lang="ts"> import { formatISO, @@ -46,41 +7,33 @@ import { fromUnixTime, getTime, getUnixTime, - parseISO, - parseJSON, isDate, isValid, + parseISO, + parseJSON, } from 'date-fns'; -import { withDefaultOnError } from '@/utils/defaults'; -import { useValidation } from '@/composable/validation'; import type { DateFormat, ToDateMapper } from './date-time-converter.types'; import { isISO8601DateTimeString, isISO9075DateString, + isMongoObjectId, isRFC3339DateString, isRFC7231DateString, isTimestamp, isUTCDateString, isUnixTimestamp, - isMongoObjectId, } from './date-time-converter.models'; +import { withDefaultOnError } from '@/utils/defaults'; +import { useValidation } from '@/composable/validation'; const inputDate = ref(''); -const toDate: ToDateMapper = (date) => new Date(date); - -function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) { - if (!date || !validation.isValid) { - return ''; - } - - return withDefaultOnError(() => formatter(date), ''); -} +const toDate: ToDateMapper = date => new Date(date); const formats: DateFormat[] = [ { name: 'JS locale date string', - fromDate: (date) => date.toString(), + fromDate: date => date.toString(), toDate, formatMatcher: () => false, }, @@ -88,49 +41,49 @@ const formats: DateFormat[] = [ name: 'ISO 8601', fromDate: formatISO, toDate: parseISO, - formatMatcher: (date) => isISO8601DateTimeString(date), + formatMatcher: date => isISO8601DateTimeString(date), }, { name: 'ISO 9075', fromDate: formatISO9075, toDate: parseISO, - formatMatcher: (date) => isISO9075DateString(date), + formatMatcher: date => isISO9075DateString(date), }, { name: 'RFC 3339', fromDate: formatRFC3339, toDate, - formatMatcher: (date) => isRFC3339DateString(date), + formatMatcher: date => isRFC3339DateString(date), }, { name: 'RFC 7231', fromDate: formatRFC7231, toDate, - formatMatcher: (date) => isRFC7231DateString(date), + formatMatcher: date => isRFC7231DateString(date), }, { name: 'Unix timestamp', - fromDate: (date) => String(getUnixTime(date)), - toDate: (sec) => fromUnixTime(+sec), - formatMatcher: (date) => isUnixTimestamp(date), + fromDate: date => String(getUnixTime(date)), + toDate: sec => fromUnixTime(+sec), + formatMatcher: date => isUnixTimestamp(date), }, { name: 'Timestamp', - fromDate: (date) => String(getTime(date)), - toDate: (ms) => parseJSON(+ms), - formatMatcher: (date) => isTimestamp(date), + fromDate: date => String(getTime(date)), + toDate: ms => parseJSON(+ms), + formatMatcher: date => isTimestamp(date), }, { name: 'UTC format', - fromDate: (date) => date.toUTCString(), + fromDate: date => date.toUTCString(), toDate, - formatMatcher: (date) => isUTCDateString(date), + formatMatcher: date => isUTCDateString(date), }, { name: 'Mongo ObjectID', - fromDate: (date) => Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000', - toDate: (objectId) => new Date(parseInt(objectId.substring(0, 8), 16) * 1000), - formatMatcher: (date) => isMongoObjectId(date), + fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`, + toDate: objectId => new Date(parseInt(objectId.substring(0, 8), 16) * 1000), + formatMatcher: date => isMongoObjectId(date), }, ]; @@ -146,7 +99,8 @@ const normalizedDate = computed(() => { try { return toDate(inputDate.value); - } catch (_ignored) { + } + catch (_ignored) { return undefined; } }); @@ -164,9 +118,11 @@ const validation = useValidation({ rules: [ { message: 'This date is invalid for this format', - validator: (value) => + validator: value => withDefaultOnError(() => { - if (value === '') return true; + if (value === '') { + return true; + } const maybeDate = formats[formatIndex.value].toDate(value); return isDate(maybeDate) && isValid(maybeDate); @@ -174,4 +130,51 @@ const validation = useValidation({ }, ], }); + +function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) { + if (!date || !validation.isValid) { + return ''; + } + + return withDefaultOnError(() => formatter(date), ''); +} </script> + +<template> + <div> + <n-input-group> + <c-input-text + v-model:value="inputDate" + autofocus + placeholder="Put you date string here..." + clearable + test-id="date-time-converter-input" + :validation="validation" + @update:value="onDateInputChanged" + /> + + <n-select + v-model:value="formatIndex" + style="flex: 0 0 170px" + :options="formats.map(({ name }, i) => ({ label: name, value: i }))" + data-test-id="date-time-converter-format-select" + /> + </n-input-group> + + <n-divider /> + + <input-copyable + v-for="{ name, fromDate } in formats" + :key="name" + :label="name" + label-width="150px" + label-position="left" + label-align="right" + :value="formatDateUsingFormatter(fromDate, normalizedDate)" + placeholder="Invalid date..." + :test-id="name" + readonly + mt-2 + /> + </div> +</template> diff --git a/src/tools/device-information/device-information.vue b/src/tools/device-information/device-information.vue index 09beced..aac0f35 100644 --- a/src/tools/device-information/device-information.vue +++ b/src/tools/device-information/device-information.vue @@ -1,22 +1,3 @@ -<template> - <c-card v-for="{ name, information } in sections" :key="name" :title="name"> - <n-grid cols="1 400:2" x-gap="12" y-gap="12"> - <n-gi v-for="{ label, value: { value } } in information" :key="label" class="information"> - <div class="label"> - {{ label }} - </div> - - <div class="value"> - <n-ellipsis v-if="value"> - {{ value }} - </n-ellipsis> - <div v-else class="undefined-value">unknown</div> - </div> - </n-gi> - </n-grid> - </c-card> -</template> - <script setup lang="ts"> import { useWindowSize } from '@vueuse/core'; import { computed } from 'vue'; @@ -77,6 +58,27 @@ const sections = [ ]; </script> +<template> + <c-card v-for="{ name, information } in sections" :key="name" :title="name"> + <n-grid cols="1 400:2" x-gap="12" y-gap="12"> + <n-gi v-for="{ label, value: { value } } in information" :key="label" class="information"> + <div class="label"> + {{ label }} + </div> + + <div class="value"> + <n-ellipsis v-if="value"> + {{ value }} + </n-ellipsis> + <div v-else class="undefined-value"> + unknown + </div> + </div> + </n-gi> + </n-grid> + </c-card> +</template> + <style lang="less" scoped> .information { padding: 14px 16px; diff --git a/src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue b/src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue index ea6e242..3503824 100644 --- a/src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue +++ b/src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue @@ -1,3 +1,34 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import { MessageType, composerize } from 'composerize-ts'; +import { withDefaultOnError } from '@/utils/defaults'; +import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; +import { textToBase64 } from '@/utils/base64'; +import TextareaCopyable from '@/components/TextareaCopyable.vue'; + +const dockerRun = ref( + 'docker run -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro --restart always --log-opt max-size=1g nginx', +); + +const conversionResult = computed(() => + withDefaultOnError(() => composerize(dockerRun.value), { yaml: '', messages: [] }), +); +const dockerCompose = computed(() => conversionResult.value.yaml); +const notImplemented = computed(() => + conversionResult.value.messages.filter(msg => msg.type === MessageType.notImplemented).map(msg => msg.value), +); +const notComposable = computed(() => + conversionResult.value.messages.filter(msg => msg.type === MessageType.notTranslatable).map(msg => msg.value), +); +const errors = computed(() => + conversionResult.value.messages + .filter(msg => msg.type === MessageType.errorDuringConversion) + .map(msg => msg.value), +); +const dockerComposeBase64 = computed(() => `data:application/yaml;base64,${textToBase64(dockerCompose.value)}`); +const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' }); +</script> + <template> <div> <n-form-item label="Your docker run command:" :show-feedback="false"> @@ -12,16 +43,20 @@ <n-divider /> - <textarea-copyable :value="dockerCompose" language="yaml" /> + <TextareaCopyable :value="dockerCompose" language="yaml" /> <div mt-5 flex justify-center> - <c-button :disabled="dockerCompose === ''" secondary @click="download"> Download docker-compose.yml </c-button> + <c-button :disabled="dockerCompose === ''" secondary @click="download"> + Download docker-compose.yml + </c-button> </div> <div v-if="notComposable.length > 0"> <n-alert title="This options are not translatable to docker-compose" type="info" mt-5> <ul> - <li v-for="(message, index) of notComposable" :key="index">{{ message }}</li> + <li v-for="(message, index) of notComposable" :key="index"> + {{ message }} + </li> </ul> </n-alert> </div> @@ -33,7 +68,9 @@ mt-5 > <ul> - <li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li> + <li v-for="(message, index) of notImplemented" :key="index"> + {{ message }} + </li> </ul> </n-alert> </div> @@ -41,41 +78,11 @@ <div v-if="errors.length > 0"> <n-alert title="The following errors occured" type="error" mt-5> <ul> - <li v-for="(message, index) of errors" :key="index">{{ message }}</li> + <li v-for="(message, index) of errors" :key="index"> + {{ message }} + </li> </ul> </n-alert> </div> </div> </template> - -<script setup lang="ts"> -import { computed, ref } from 'vue'; -import { withDefaultOnError } from '@/utils/defaults'; -import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; -import { textToBase64 } from '@/utils/base64'; -import TextareaCopyable from '@/components/TextareaCopyable.vue'; - -import { composerize, MessageType } from 'composerize-ts'; - -const dockerRun = ref( - 'docker run -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro --restart always --log-opt max-size=1g nginx', -); - -const conversionResult = computed(() => - withDefaultOnError(() => composerize(dockerRun.value), { yaml: '', messages: [] }), -); -const dockerCompose = computed(() => conversionResult.value.yaml); -const notImplemented = computed(() => - conversionResult.value.messages.filter((msg) => msg.type === MessageType.notImplemented).map((msg) => msg.value), -); -const notComposable = computed(() => - conversionResult.value.messages.filter((msg) => msg.type === MessageType.notTranslatable).map((msg) => msg.value), -); -const errors = computed(() => - conversionResult.value.messages - .filter((msg) => msg.type === MessageType.errorDuringConversion) - .map((msg) => msg.value), -); -const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value)); -const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' }); -</script> diff --git a/src/tools/encryption/encryption.vue b/src/tools/encryption/encryption.vue index 4da451c..d738509 100644 --- a/src/tools/encryption/encryption.vue +++ b/src/tools/encryption/encryption.vue @@ -1,3 +1,22 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import { AES, RC4, Rabbit, TripleDES, enc } from 'crypto-js'; + +const algos = { AES, TripleDES, Rabbit, RC4 }; + +const cypherInput = ref('Lorem ipsum dolor sit amet'); +const cypherAlgo = ref<keyof typeof algos>('AES'); +const cypherSecret = ref('my secret key'); +const cypherOutput = computed(() => algos[cypherAlgo.value].encrypt(cypherInput.value, cypherSecret.value).toString()); + +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), +); +</script> + <template> <c-card title="Encrypt"> <div flex gap-3> @@ -78,22 +97,3 @@ </n-form-item> </c-card> </template> - -<script setup lang="ts"> -import { computed, ref } from 'vue'; -import { AES, TripleDES, Rabbit, RC4, enc } from 'crypto-js'; - -const algos = { AES, TripleDES, Rabbit, RC4 }; - -const cypherInput = ref('Lorem ipsum dolor sit amet'); -const cypherAlgo = ref<keyof typeof algos>('AES'); -const cypherSecret = ref('my secret key'); -const cypherOutput = computed(() => algos[cypherAlgo.value].encrypt(cypherInput.value, cypherSecret.value).toString()); - -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), -); -</script> diff --git a/src/tools/eta-calculator/eta-calculator.vue b/src/tools/eta-calculator/eta-calculator.vue index 59bccda..76c3b45 100644 --- a/src/tools/eta-calculator/eta-calculator.vue +++ b/src/tools/eta-calculator/eta-calculator.vue @@ -1,3 +1,28 @@ +<script setup lang="ts"> +// Duplicate issue with sub directory + +import { addMilliseconds, formatRelative } from 'date-fns'; + +import { enGB } from 'date-fns/locale'; +import { computed, ref } from 'vue'; +import { formatMsDuration } from './eta-calculator.service'; + +const unitCount = ref(3 * 62); +const unitPerTimeSpan = ref(3); +const timeSpan = ref(5); +const timeSpanUnitMultiplier = ref(60000); +const startedAt = ref(Date.now()); + +const durationMs = computed(() => { + const timeSpanMs = timeSpan.value * timeSpanUnitMultiplier.value; + + return unitCount.value / (unitPerTimeSpan.value / timeSpanMs); +}); +const endAt = computed(() => + formatRelative(addMilliseconds(startedAt.value, durationMs.value), Date.now(), { locale: enGB }), +); +</script> + <template> <div> <n-text depth="3" style="text-align: justify; width: 100%; display: inline-block"> @@ -29,45 +54,24 @@ { label: 'hours', value: 1000 * 60 * 60 }, { label: 'days', value: 1000 * 60 * 60 * 24 }, ]" - ></n-select> + /> </n-input-group> </n-form-item> <n-divider /> <c-card mb-2> - <n-statistic label="Total duration">{{ formatMsDuration(durationMs) }}</n-statistic> + <n-statistic label="Total duration"> + {{ formatMsDuration(durationMs) }} + </n-statistic> </c-card> <c-card> - <n-statistic label="It will end ">{{ endAt }}</n-statistic> + <n-statistic label="It will end "> + {{ endAt }} + </n-statistic> </c-card> </div> </template> -<script setup lang="ts"> -// Duplicate issue with sub directory -// eslint-disable-next-line import/no-duplicates -import { addMilliseconds, formatRelative } from 'date-fns'; -// eslint-disable-next-line import/no-duplicates -import { enGB } from 'date-fns/locale'; -import { computed, ref } from 'vue'; -import { formatMsDuration } from './eta-calculator.service'; - -const unitCount = ref(3 * 62); -const unitPerTimeSpan = ref(3); -const timeSpan = ref(5); -const timeSpanUnitMultiplier = ref(60000); -const startedAt = ref(Date.now()); - -const durationMs = computed(() => { - const timeSpanMs = timeSpan.value * timeSpanUnitMultiplier.value; - - return unitCount.value / (unitPerTimeSpan.value / timeSpanMs); -}); -const endAt = computed(() => - formatRelative(addMilliseconds(startedAt.value, durationMs.value), Date.now(), { locale: enGB }), -); -</script> - <style lang="less" scoped> .n-input-number, .n-date-picker { diff --git a/src/tools/git-memo/git-memo.vue b/src/tools/git-memo/git-memo.vue index 8d85e99..bad8440 100644 --- a/src/tools/git-memo/git-memo.vue +++ b/src/tools/git-memo/git-memo.vue @@ -1,9 +1,3 @@ -<template> - <div> - <memo /> - </div> -</template> - <script setup lang="ts"> import { useThemeVars } from 'naive-ui'; import Memo from './git-memo.md'; @@ -11,6 +5,12 @@ import Memo from './git-memo.md'; const themeVars = useThemeVars(); </script> +<template> + <div> + <Memo /> + </div> +</template> + <style lang="less" scoped> ::v-deep(pre) { margin: 0; diff --git a/src/tools/hash-text/hash-text.service.ts b/src/tools/hash-text/hash-text.service.ts index f1241d1..6b9b014 100644 --- a/src/tools/hash-text/hash-text.service.ts +++ b/src/tools/hash-text/hash-text.service.ts @@ -2,6 +2,6 @@ export function convertHexToBin(hex: string) { return hex .trim() .split('') - .map((byte) => parseInt(byte, 16).toString(2).padStart(4, '0')) + .map(byte => parseInt(byte, 16).toString(2).padStart(4, '0')) .join(''); } diff --git a/src/tools/hash-text/hash-text.vue b/src/tools/hash-text/hash-text.vue index b57d89c..b43ed6c 100644 --- a/src/tools/hash-text/hash-text.vue +++ b/src/tools/hash-text/hash-text.vue @@ -1,3 +1,39 @@ +<script setup lang="ts"> +import type { lib } from 'crypto-js'; +import { MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512, enc } from 'crypto-js'; +import { ref } from 'vue'; +import InputCopyable from '../../components/InputCopyable.vue'; +import { convertHexToBin } from './hash-text.service'; +import { useQueryParam } from '@/composable/queryParams'; + +const algos = { + MD5, + SHA1, + SHA256, + SHA224, + SHA512, + SHA384, + SHA3, + RIPEMD160, +} as const; + +type AlgoNames = keyof typeof algos; +type Encoding = keyof typeof enc | 'Bin'; +const algoNames = Object.keys(algos) as AlgoNames[]; +const encoding = useQueryParam<Encoding>({ defaultValue: 'Hex', name: 'encoding' }); +const clearText = ref(''); + +function formatWithEncoding(words: lib.WordArray, encoding: Encoding) { + if (encoding === 'Bin') { + return convertHexToBin(words.toString(enc.Hex)); + } + + return words.toString(enc[encoding]); +} + +const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[algo](value), encoding.value); +</script> + <template> <div> <c-card> @@ -31,45 +67,12 @@ <div v-for="algo in algoNames" :key="algo" style="margin: 5px 0"> <n-input-group> - <n-input-group-label style="flex: 0 0 120px"> {{ algo }} </n-input-group-label> - <input-copyable :value="hashText(algo, clearText)" readonly /> + <n-input-group-label style="flex: 0 0 120px"> + {{ algo }} + </n-input-group-label> + <InputCopyable :value="hashText(algo, clearText)" readonly /> </n-input-group> </div> </c-card> </div> </template> - -<script setup lang="ts"> -import { useQueryParam } from '@/composable/queryParams'; -import { enc, lib, MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512 } from 'crypto-js'; -import { ref } from 'vue'; -import InputCopyable from '../../components/InputCopyable.vue'; -import { convertHexToBin } from './hash-text.service'; - -const algos = { - MD5, - SHA1, - SHA256, - SHA224, - SHA512, - SHA384, - SHA3, - RIPEMD160, -} as const; - -type AlgoNames = keyof typeof algos; -type Encoding = keyof typeof enc | 'Bin'; -const algoNames = Object.keys(algos) as AlgoNames[]; -const encoding = useQueryParam<Encoding>({ defaultValue: 'Hex', name: 'encoding' }); -const clearText = ref(''); - -function formatWithEncoding(words: lib.WordArray, encoding: Encoding) { - if (encoding === 'Bin') { - return convertHexToBin(words.toString(enc.Hex)); - } - - return words.toString(enc[encoding]); -} - -const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[algo](value), encoding.value); -</script> diff --git a/src/tools/hmac-generator/hmac-generator.vue b/src/tools/hmac-generator/hmac-generator.vue index f0fc239..6e6b1f1 100644 --- a/src/tools/hmac-generator/hmac-generator.vue +++ b/src/tools/hmac-generator/hmac-generator.vue @@ -1,3 +1,50 @@ +<script setup lang="ts"> +import type { lib } from 'crypto-js'; +import { + HmacMD5, + HmacRIPEMD160, + HmacSHA1, + HmacSHA224, + HmacSHA256, + HmacSHA3, + HmacSHA384, + HmacSHA512, + enc, +} from 'crypto-js'; +import { computed, ref } from 'vue'; +import { convertHexToBin } from '../hash-text/hash-text.service'; +import { useCopy } from '@/composable/copy'; + +const algos = { + MD5: HmacMD5, + RIPEMD160: HmacRIPEMD160, + SHA1: HmacSHA1, + SHA3: HmacSHA3, + SHA224: HmacSHA224, + SHA256: HmacSHA256, + SHA384: HmacSHA384, + SHA512: HmacSHA512, +} as const; + +type Encoding = keyof typeof enc | 'Bin'; + +function formatWithEncoding(words: lib.WordArray, encoding: Encoding) { + if (encoding === 'Bin') { + return convertHexToBin(words.toString(enc.Hex)); + } + return words.toString(enc[encoding]); +} + +const plainText = ref(''); +const secret = ref(''); +const hashFunction = ref<keyof typeof algos>('SHA256'); +const encoding = ref<Encoding>('Hex'); +const hmac = computed(() => + formatWithEncoding(algos[hashFunction.value](plainText.value, secret.value), encoding.value), +); +const { copy } = useCopy({ source: hmac }); +</script> + <template> <div> <n-form-item label="Plain text to compute the hash"> @@ -43,54 +90,9 @@ <n-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." /> </n-form-item> <div flex justify-center> - <c-button @click="copy()">Copy HMAC</c-button> + <c-button @click="copy()"> + Copy HMAC + </c-button> </div> </div> </template> - -<script setup lang="ts"> -import { useCopy } from '@/composable/copy'; -import { - enc, - HmacMD5, - HmacRIPEMD160, - HmacSHA1, - HmacSHA224, - HmacSHA256, - HmacSHA3, - HmacSHA384, - HmacSHA512, - lib, -} from 'crypto-js'; -import { computed, ref } from 'vue'; -import { convertHexToBin } from '../hash-text/hash-text.service'; - -const algos = { - MD5: HmacMD5, - RIPEMD160: HmacRIPEMD160, - SHA1: HmacSHA1, - SHA3: HmacSHA3, - SHA224: HmacSHA224, - SHA256: HmacSHA256, - SHA384: HmacSHA384, - SHA512: HmacSHA512, -} as const; - -type Encoding = keyof typeof enc | 'Bin'; - -function formatWithEncoding(words: lib.WordArray, encoding: Encoding) { - if (encoding === 'Bin') { - return convertHexToBin(words.toString(enc.Hex)); - } - return words.toString(enc[encoding]); -} - -const plainText = ref(''); -const secret = ref(''); -const hashFunction = ref<keyof typeof algos>('SHA256'); -const encoding = ref<Encoding>('Hex'); -const hmac = computed(() => - formatWithEncoding(algos[hashFunction.value](plainText.value, secret.value), encoding.value), -); -const { copy } = useCopy({ source: hmac }); -</script> diff --git a/src/tools/html-entities/html-entities.vue b/src/tools/html-entities/html-entities.vue index 0c75975..408542a 100644 --- a/src/tools/html-entities/html-entities.vue +++ b/src/tools/html-entities/html-entities.vue @@ -1,3 +1,17 @@ +<script setup lang="ts"> +import { escape, unescape } from 'lodash'; +import { computed, ref } from 'vue'; +import { useCopy } from '@/composable/copy'; + +const escapeInput = ref('<title>IT Tool</title>'); +const escapeOutput = computed(() => escape(escapeInput.value)); +const { copy: copyEscaped } = useCopy({ source: escapeOutput }); + +const unescapeInput = ref('<title>IT Tool</title'); +const unescapeOutput = computed(() => unescape(unescapeInput.value)); +const { copy: copyUnescaped } = useCopy({ source: unescapeOutput }); +</script> + <template> <c-card title="Escape html entities"> <n-form-item label="Your string :"> @@ -20,7 +34,9 @@ </n-form-item> <div flex justify-center> - <c-button @click="copyEscaped"> Copy </c-button> + <c-button @click="copyEscaped"> + Copy + </c-button> </div> </c-card> <c-card title="Unescape html entities"> @@ -44,21 +60,9 @@ </n-form-item> <div flex justify-center> - <c-button @click="copyUnescaped"> Copy </c-button> + <c-button @click="copyUnescaped"> + Copy + </c-button> </div> </c-card> </template> - -<script setup lang="ts"> -import { escape, unescape } from 'lodash'; -import { computed, ref } from 'vue'; -import { useCopy } from '@/composable/copy'; - -const escapeInput = ref('<title>IT Tool</title>'); -const escapeOutput = computed(() => escape(escapeInput.value)); -const { copy: copyEscaped } = useCopy({ source: escapeOutput }); - -const unescapeInput = ref('<title>IT Tool</title'); -const unescapeOutput = computed(() => unescape(unescapeInput.value)); -const { copy: copyUnescaped } = useCopy({ source: unescapeOutput }); -</script> diff --git a/src/tools/html-wysiwyg-editor/editor/editor.vue b/src/tools/html-wysiwyg-editor/editor/editor.vue index 4c8ca36..86bdebb 100644 --- a/src/tools/html-wysiwyg-editor/editor/editor.vue +++ b/src/tools/html-wysiwyg-editor/editor/editor.vue @@ -1,14 +1,3 @@ -<template> - <c-card v-if="editor" important:p0> - <menu-bar class="editor-header" :editor="editor" /> - <n-divider style="margin-top: 0" /> - - <div px8 pb6> - <editor-content class="editor-content" :editor="editor" /> - </div> - </c-card> -</template> - <script setup lang="ts"> import { tryOnBeforeUnmount, useVModel } from '@vueuse/core'; import { Editor, EditorContent } from '@tiptap/vue-3'; @@ -16,9 +5,9 @@ import StarterKit from '@tiptap/starter-kit'; import { useThemeVars } from 'naive-ui'; import MenuBar from './menu-bar.vue'; -const themeVars = useThemeVars(); const props = defineProps<{ html: string }>(); const emit = defineEmits(['update:html']); +const themeVars = useThemeVars(); const html = useVModel(props, 'html', emit); const editor = new Editor({ @@ -33,6 +22,17 @@ tryOnBeforeUnmount(() => { }); </script> +<template> + <c-card v-if="editor" important:p0> + <MenuBar class="editor-header" :editor="editor" /> + <n-divider style="margin-top: 0" /> + + <div px8 pb6> + <EditorContent class="editor-content" :editor="editor" /> + </div> + </c-card> +</template> + <style scoped lang="less"> ::v-deep(.ProseMirror-focused) { outline: none; diff --git a/src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue b/src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue index a185618..85d72e6 100644 --- a/src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue +++ b/src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue @@ -1,3 +1,10 @@ +<script setup lang="ts"> +import { type Component, toRefs } from 'vue'; + +const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean }>(); +const { icon, title, action, isActive } = toRefs(props); +</script> + <template> <n-tooltip trigger="hover"> <template #trigger> @@ -9,12 +16,3 @@ {{ title }} </n-tooltip> </template> - -<script setup lang="ts"> -import { toRefs, type Component } from 'vue'; - -const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean }>(); -const { icon, title, action, isActive } = toRefs(props); -</script> - -<style scoped></style> diff --git a/src/tools/html-wysiwyg-editor/editor/menu-bar.vue b/src/tools/html-wysiwyg-editor/editor/menu-bar.vue index b54d97b..01ba9c7 100644 --- a/src/tools/html-wysiwyg-editor/editor/menu-bar.vue +++ b/src/tools/html-wysiwyg-editor/editor/menu-bar.vue @@ -1,12 +1,3 @@ -<template> - <div flex items-center> - <template v-for="(item, index) in items"> - <n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical /> - <menu-bar-item v-else-if="item.type === 'button'" :key="index" v-bind="item" /> - </template> - </div> -</template> - <script setup lang="ts"> import type { Editor } from '@tiptap/vue-3'; import { @@ -27,7 +18,7 @@ import { Strikethrough, TextWrap, } from '@vicons/tabler'; -import { toRefs, type Component } from 'vue'; +import { type Component, toRefs } from 'vue'; import MenuBarItem from './menu-bar-item.vue'; const props = defineProps<{ editor: Editor }>(); @@ -35,12 +26,12 @@ const { editor } = toRefs(props); type MenuItem = | { - icon: Component; - title: string; - action: () => void; - isActive?: () => boolean; - type: 'button'; - } + icon: Component + title: string + action: () => void + isActive?: () => boolean + type: 'button' + } | { type: 'divider' }; const items: MenuItem[] = [ @@ -166,4 +157,11 @@ const items: MenuItem[] = [ ]; </script> -<style scoped></style> +<template> + <div flex items-center> + <template v-for="(item, index) in items"> + <n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical /> + <MenuBarItem v-else-if="item.type === 'button'" :key="index" v-bind="item" /> + </template> + </div> +</template> diff --git a/src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue b/src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue index 7cd1256..6923821 100644 --- a/src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue +++ b/src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue @@ -1,16 +1,14 @@ -<template> - <editor v-model:html="html" /> - <textarea-copyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" /> -</template> - <script setup lang="ts"> -import TextareaCopyable from '@/components/TextareaCopyable.vue'; import { format } from 'prettier'; import htmlParser from 'prettier/parser-html'; import { useStorage } from '@vueuse/core'; import Editor from './editor/editor.vue'; +import TextareaCopyable from '@/components/TextareaCopyable.vue'; const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>'); </script> -<style lang="less" scoped></style> +<template> + <Editor v-model:html="html" /> + <TextareaCopyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" /> +</template> diff --git a/src/tools/http-status-codes/http-status-codes.constants.ts b/src/tools/http-status-codes/http-status-codes.constants.ts index c64bda0..279cd7c 100644 --- a/src/tools/http-status-codes/http-status-codes.constants.ts +++ b/src/tools/http-status-codes/http-status-codes.constants.ts @@ -1,11 +1,11 @@ export const codesByCategories: { - category: string; + category: string codes: { - code: number; - name: string; - description: string; - type: 'HTTP' | 'WebDav'; - }[]; + code: number + name: string + description: string + type: 'HTTP' | 'WebDav' + }[] }[] = [ { category: '1xx informational response', @@ -286,7 +286,7 @@ export const codesByCategories: { }, { code: 418, - name: "I'm a teapot", + name: 'I\'m a teapot', description: 'The server refuses the attempt to brew coffee with a teapot.', type: 'HTTP', }, diff --git a/src/tools/http-status-codes/http-status-codes.e2e.spec.ts b/src/tools/http-status-codes/http-status-codes.e2e.spec.ts index 59979cd..cf6eae9 100644 --- a/src/tools/http-status-codes/http-status-codes.e2e.spec.ts +++ b/src/tools/http-status-codes/http-status-codes.e2e.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.describe('Tool - Http status codes', () => { test.beforeEach(async ({ page }) => { diff --git a/src/tools/http-status-codes/http-status-codes.vue b/src/tools/http-status-codes/http-status-codes.vue index 9f827f5..0fb1125 100644 --- a/src/tools/http-status-codes/http-status-codes.vue +++ b/src/tools/http-status-codes/http-status-codes.vue @@ -1,3 +1,27 @@ +<script setup lang="ts"> +import { SearchRound } from '@vicons/material'; +import { codesByCategories } from './http-status-codes.constants'; +import { useFuzzySearch } from '@/composable/fuzzySearch'; + +const search = ref(''); + +const { searchResult } = useFuzzySearch({ + search, + data: codesByCategories.flatMap(({ codes, category }) => codes.map(code => ({ ...code, category }))), + options: { + keys: [{ name: 'code', weight: 3 }, { name: 'name', weight: 2 }, 'description', 'category'], + }, +}); + +const codesByCategoryFiltered = computed(() => { + if (!search.value) { + return codesByCategories; + } + + return [{ category: 'Search results', codes: searchResult.value }]; +}); +</script> + <template> <div> <n-form-item :show-label="false"> @@ -21,35 +45,13 @@ <n-h2> {{ category }} </n-h2> <c-card v-for="{ code, description, name, type } of codes" :key="code" mb-2> - <n-text strong block text-lg> {{ code }} {{ name }} </n-text> - <n-text block depth="3">{{ description }} {{ type !== 'HTTP' ? `For ${type}.` : '' }}</n-text> + <n-text strong block text-lg> + {{ code }} {{ name }} + </n-text> + <n-text block depth="3"> + {{ description }} {{ type !== 'HTTP' ? `For ${type}.` : '' }} + </n-text> </c-card> </div> </div> </template> - -<script setup lang="ts"> -import { useFuzzySearch } from '@/composable/fuzzySearch'; -import { SearchRound } from '@vicons/material'; -import { codesByCategories } from './http-status-codes.constants'; - -const search = ref(''); - -const { searchResult } = useFuzzySearch({ - search, - data: codesByCategories.flatMap(({ codes, category }) => codes.map((code) => ({ ...code, category }))), - options: { - keys: [{ name: 'code', weight: 3 }, { name: 'name', weight: 2 }, 'description', 'category'], - }, -}); - -const codesByCategoryFiltered = computed(() => { - if (!search.value) { - return codesByCategories; - } - - return [{ category: 'Search results', codes: searchResult.value }]; -}); -</script> - -<style lang="less" scoped></style> diff --git a/src/tools/index.ts b/src/tools/index.ts index 44ef8a3..211a8a8 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -140,5 +140,5 @@ export const toolsByCategory: ToolCategory[] = [ export const tools = toolsByCategory.flatMap(({ components }) => components); export const toolsWithCategory = toolsByCategory.flatMap(({ components, name }) => - components.map((tool) => ({ category: name, ...tool })), + components.map(tool => ({ category: name, ...tool })), ); diff --git a/src/tools/integer-base-converter/integer-base-converter.model.test.ts b/src/tools/integer-base-converter/integer-base-converter.model.test.ts index e9d91f6..d0387b6 100644 --- a/src/tools/integer-base-converter/integer-base-converter.model.test.ts +++ b/src/tools/integer-base-converter/integer-base-converter.model.test.ts @@ -1,4 +1,4 @@ -import { expect, describe, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { convertBase } from './integer-base-converter.model'; describe('integer-base-converter', () => { diff --git a/src/tools/integer-base-converter/integer-base-converter.model.ts b/src/tools/integer-base-converter/integer-base-converter.model.ts index cfe15bd..b4470e5 100644 --- a/src/tools/integer-base-converter/integer-base-converter.model.ts +++ b/src/tools/integer-base-converter/integer-base-converter.model.ts @@ -7,9 +7,9 @@ export function convertBase({ value, fromBase, toBase }: { value: string; fromBa .reverse() .reduce((carry: number, digit: string, index: number) => { 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) * fromBase ** index); }, 0); let newValue = ''; while (decValue > 0) { diff --git a/src/tools/integer-base-converter/integer-base-converter.vue b/src/tools/integer-base-converter/integer-base-converter.vue index 058d831..01f6401 100644 --- a/src/tools/integer-base-converter/integer-base-converter.vue +++ b/src/tools/integer-base-converter/integer-base-converter.vue @@ -1,56 +1,103 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import InputCopyable from '../../components/InputCopyable.vue'; +import { convertBase } from './integer-base-converter.model'; +import { useStyleStore } from '@/stores/style.store'; +import { getErrorMessageIfThrows } from '@/utils/error'; + +const styleStore = useStyleStore(); + +const inputProps = { + 'labelPosition': 'left', + 'labelWidth': '170px', + 'labelAlign': 'right', + 'readonly': true, + 'mb-2': '', +} as const; + +const input = ref('42'); +const inputBase = ref(10); +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> + <template> <div> <c-card> <div v-if="styleStore.isSmallScreen"> <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" w-full :status="error ? 'error' : undefined" /> </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> <n-input-number v-model:value="inputBase" max="64" min="2" w-full /> </n-input-group> </div> <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-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-group> - <n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert> + <n-alert v-if="error" style="margin-top: 25px" type="error"> + {{ error }} + </n-alert> <n-divider /> - <input-copyable + <InputCopyable label="Binary (2)" v-bind="inputProps" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })" placeholder="Binary version will be here..." /> - <input-copyable + <InputCopyable label="Octal (8)" v-bind="inputProps" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })" placeholder="Octal version will be here..." /> - <input-copyable + <InputCopyable label="Decimal (10)" v-bind="inputProps" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })" placeholder="Decimal version will be here..." /> - <input-copyable + <InputCopyable label="Hexadecimal (16)" v-bind="inputProps" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })" placeholder="Hexadecimal version will be here..." /> - <input-copyable + <InputCopyable label="Base64 (64)" v-bind="inputProps" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })" @@ -63,7 +110,7 @@ <n-input-number v-model:value="outputBase" max="64" min="2" /> </n-input-group> - <input-copyable + <InputCopyable flex-1 v-bind="inputProps" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })" @@ -74,42 +121,6 @@ </div> </template> -<script setup lang="ts"> -import { computed, ref } from 'vue'; -import { useStyleStore } from '@/stores/style.store'; -import { getErrorMessageIfThrows } from '@/utils/error'; -import { convertBase } from './integer-base-converter.model'; -import InputCopyable from '../../components/InputCopyable.vue'; - -const styleStore = useStyleStore(); - -const inputProps = { - labelPosition: 'left', - labelWidth: '170px', - labelAlign: 'right', - readonly: true, - 'mb-2': '', -} as const; - -const input = ref('42'); -const inputBase = ref(10); -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> - <style lang="less" scoped> .n-input-group:not(:first-child) { margin-top: 5px; diff --git a/src/tools/ipv4-address-converter/ipv4-address-converter.service.test.ts b/src/tools/ipv4-address-converter/ipv4-address-converter.service.test.ts index ecdcfa2..4dcf90d 100644 --- a/src/tools/ipv4-address-converter/ipv4-address-converter.service.test.ts +++ b/src/tools/ipv4-address-converter/ipv4-address-converter.service.test.ts @@ -1,5 +1,5 @@ -import { expect, describe, it } from 'vitest'; -import { isValidIpv4, ipv4ToInt } from './ipv4-address-converter.service'; +import { describe, expect, it } from 'vitest'; +import { ipv4ToInt, isValidIpv4 } from './ipv4-address-converter.service'; describe('ipv4-address-converter', () => { describe('ipv4ToInt', () => { diff --git a/src/tools/ipv4-address-converter/ipv4-address-converter.service.ts b/src/tools/ipv4-address-converter/ipv4-address-converter.service.ts index ffd5d80..1ef487e 100644 --- a/src/tools/ipv4-address-converter/ipv4-address-converter.service.ts +++ b/src/tools/ipv4-address-converter/ipv4-address-converter.service.ts @@ -10,7 +10,7 @@ function ipv4ToInt({ ip }: { ip: string }) { return ip .trim() .split('.') - .reduce((acc, part, index) => acc + Number(part) * Math.pow(256, 3 - index), 0); + .reduce((acc, part, index) => acc + Number(part) * 256 ** (3 - index), 0); } function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: string; prefix?: string }) { @@ -19,13 +19,13 @@ function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: str } return ( - prefix + - _.chain(ip) + prefix + + _.chain(ip) .trim() .split('.') - .map((part) => parseInt(part).toString(16).padStart(2, '0')) + .map(part => parseInt(part).toString(16).padStart(2, '0')) .chunk(2) - .map((blocks) => blocks.join('')) + .map(blocks => blocks.join('')) .join(':') .value() ); diff --git a/src/tools/ipv4-address-converter/ipv4-address-converter.vue b/src/tools/ipv4-address-converter/ipv4-address-converter.vue index dce1dbe..40b1890 100644 --- a/src/tools/ipv4-address-converter/ipv4-address-converter.vue +++ b/src/tools/ipv4-address-converter/ipv4-address-converter.vue @@ -1,27 +1,7 @@ -<template> - <div> - <c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." /> - - <n-divider /> - - <input-copyable - v-for="{ label, value } of convertedSections" - :key="label" - :label="label" - label-position="left" - label-width="100px" - label-align="right" - mb-2 - :value="validationAttrs.validationStatus === 'error' ? '' : value" - placeholder="Set a correct ipv4 address" - /> - </div> -</template> - <script setup lang="ts"> -import { useValidation } from '@/composable/validation'; import { convertBase } from '../integer-base-converter/integer-base-converter.model'; import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from './ipv4-address-converter.service'; +import { useValidation } from '@/composable/validation'; const rawIpAddress = useStorage('ipv4-converter:ip', '192.168.1.1'); @@ -54,8 +34,26 @@ const convertedSections = computed(() => { const { attrs: validationAttrs } = useValidation({ source: rawIpAddress, - rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }], + rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }], }); </script> -<style lang="less" scoped></style> +<template> + <div> + <c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." /> + + <n-divider /> + + <input-copyable + v-for="{ label, value } of convertedSections" + :key="label" + :label="label" + label-position="left" + label-width="100px" + label-align="right" + mb-2 + :value="validationAttrs.validationStatus === 'error' ? '' : value" + placeholder="Set a correct ipv4 address" + /> + </div> +</template> diff --git a/src/tools/ipv4-range-expander/ipv4-range-expander.e2e.spec.ts b/src/tools/ipv4-range-expander/ipv4-range-expander.e2e.spec.ts index b919827..7d9a4a8 100644 --- a/src/tools/ipv4-range-expander/ipv4-range-expander.e2e.spec.ts +++ b/src/tools/ipv4-range-expander/ipv4-range-expander.e2e.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.describe('Tool - IPv4 range expander', () => { test.beforeEach(async ({ page }) => { diff --git a/src/tools/ipv4-range-expander/ipv4-range-expander.service.test.ts b/src/tools/ipv4-range-expander/ipv4-range-expander.service.test.ts index 6888ac1..5104623 100644 --- a/src/tools/ipv4-range-expander/ipv4-range-expander.service.test.ts +++ b/src/tools/ipv4-range-expander/ipv4-range-expander.service.test.ts @@ -1,4 +1,4 @@ -import { expect, describe, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { calculateCidr } from './ipv4-range-expander.service'; describe('ipv4RangeExpander', () => { diff --git a/src/tools/ipv4-range-expander/ipv4-range-expander.service.ts b/src/tools/ipv4-range-expander/ipv4-range-expander.service.ts index 7dd2c64..78fbde5 100644 --- a/src/tools/ipv4-range-expander/ipv4-range-expander.service.ts +++ b/src/tools/ipv4-range-expander/ipv4-range-expander.service.ts @@ -1,20 +1,25 @@ -import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types'; import { convertBase } from '../integer-base-converter/integer-base-converter.model'; import { ipv4ToInt } from '../ipv4-address-converter/ipv4-address-converter.service'; +import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types'; + export { calculateCidr }; function bits2ip(ipInt: number) { - return (ipInt >>> 24) + '.' + ((ipInt >> 16) & 255) + '.' + ((ipInt >> 8) & 255) + '.' + (ipInt & 255); + return `${ipInt >>> 24}.${(ipInt >> 16) & 255}.${(ipInt >> 8) & 255}.${ipInt & 255}`; } function getRangesize(start: string, end: string) { - if (start == null || end == null) return -1; + if (start == null || end == null) { + return -1; + } return 1 + parseInt(end, 2) - parseInt(start, 2); } function getCidr(start: string, end: string) { - if (start == null || end == null) return null; + if (start == null || end == null) { + return null; + } const range = getRangesize(start, end); if (range < 1) { @@ -32,7 +37,7 @@ function getCidr(start: string, end: string) { const newStart = start.substring(0, mask) + '0'.repeat(32 - mask); const newEnd = end.substring(0, mask) + '1'.repeat(32 - mask); - return { start: newStart, end: newEnd, mask: mask }; + return { start: newStart, end: newEnd, mask }; } function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) { @@ -52,7 +57,7 @@ function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) { const result: Ipv4RangeExpanderResult = {}; result.newEnd = bits2ip(parseInt(cidr.end, 2)); result.newStart = bits2ip(parseInt(cidr.start, 2)); - result.newCidr = result.newStart + '/' + cidr.mask; + result.newCidr = `${result.newStart}/${cidr.mask}`; result.newSize = getRangesize(cidr.start, cidr.end); result.oldSize = getRangesize(start, end); diff --git a/src/tools/ipv4-range-expander/ipv4-range-expander.types.ts b/src/tools/ipv4-range-expander/ipv4-range-expander.types.ts index 9172fed..d5d2b45 100644 --- a/src/tools/ipv4-range-expander/ipv4-range-expander.types.ts +++ b/src/tools/ipv4-range-expander/ipv4-range-expander.types.ts @@ -1,7 +1,7 @@ -export type Ipv4RangeExpanderResult = { - oldSize?: number; - newStart?: string; - newEnd?: string; - newCidr?: string; - newSize?: number; -}; +export interface Ipv4RangeExpanderResult { + oldSize?: number + newStart?: string + newEnd?: string + newCidr?: string + newSize?: number +} diff --git a/src/tools/ipv4-range-expander/ipv4-range-expander.vue b/src/tools/ipv4-range-expander/ipv4-range-expander.vue index f1b8dc6..16ce62f 100644 --- a/src/tools/ipv4-range-expander/ipv4-range-expander.vue +++ b/src/tools/ipv4-range-expander/ipv4-range-expander.vue @@ -1,3 +1,61 @@ +<script setup lang="ts"> +import { Exchange } from '@vicons/tabler'; +import { isValidIpv4 } from '../ipv4-address-converter/ipv4-address-converter.service'; +import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types'; +import { calculateCidr } from './ipv4-range-expander.service'; +import ResultRow from './result-row.vue'; +import { useValidation } from '@/composable/validation'; + +const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1'); +const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255'); + +const result = computed(() => calculateCidr({ startIp: rawStartAddress.value, endIp: rawEndAddress.value })); + +const calculatedValues: { + label: string + getOldValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined + getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined +}[] = [ + { + label: 'Start address', + getOldValue: () => rawStartAddress.value, + getNewValue: result => result?.newStart, + }, + { + label: 'End address', + getOldValue: () => rawEndAddress.value, + getNewValue: result => result?.newEnd, + }, + { + label: 'Addresses in range', + getOldValue: result => result?.oldSize?.toLocaleString(), + getNewValue: result => result?.newSize?.toLocaleString(), + }, + { + label: 'CIDR', + getOldValue: () => '', + getNewValue: result => result?.newCidr, + }, +]; + +const startIpValidation = useValidation({ + source: rawStartAddress, + rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }], +}); +const endIpValidation = useValidation({ + source: rawEndAddress, + rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }], +}); + +const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined); + +function onSwitchStartEndClicked() { + const tmpStart = rawStartAddress.value; + rawStartAddress.value = rawEndAddress.value; + rawEndAddress.value = tmpStart; +} +</script> + <template> <div> <div mb-4 flex gap-4> @@ -21,13 +79,19 @@ <n-table v-if="showResult" data-test-id="result"> <thead> <tr> - <th scope="col"> </th> - <th scope="col">old value</th> - <th scope="col">new value</th> + <th scope="col"> + + </th> + <th scope="col"> + old value + </th> + <th scope="col"> + new value + </th> </tr> </thead> <tbody> - <result-row + <ResultRow v-for="{ label, getOldValue, getNewValue } in calculatedValues" :key="label" :label="label" @@ -53,62 +117,3 @@ </n-alert> </div> </template> - -<script setup lang="ts"> -import { useValidation } from '@/composable/validation'; -import { Exchange } from '@vicons/tabler'; -import { isValidIpv4 } from '../ipv4-address-converter/ipv4-address-converter.service'; -import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types'; -import { calculateCidr } from './ipv4-range-expander.service'; -import ResultRow from './result-row.vue'; - -const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1'); -const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255'); - -const result = computed(() => calculateCidr({ startIp: rawStartAddress.value, endIp: rawEndAddress.value })); - -const calculatedValues: { - label: string; - getOldValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined; - getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined; -}[] = [ - { - label: 'Start address', - getOldValue: () => rawStartAddress.value, - getNewValue: (result) => result?.newStart, - }, - { - label: 'End address', - getOldValue: () => rawEndAddress.value, - getNewValue: (result) => result?.newEnd, - }, - { - label: 'Addresses in range', - getOldValue: (result) => result?.oldSize?.toLocaleString(), - getNewValue: (result) => result?.newSize?.toLocaleString(), - }, - { - label: 'CIDR', - getOldValue: () => '', - getNewValue: (result) => result?.newCidr, - }, -]; - -const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined); -const startIpValidation = useValidation({ - source: rawStartAddress, - rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }], -}); -const endIpValidation = useValidation({ - source: rawEndAddress, - rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }], -}); - -function onSwitchStartEndClicked() { - const tmpStart = rawStartAddress.value; - rawStartAddress.value = rawEndAddress.value; - rawEndAddress.value = tmpStart; -} -</script> - -<style lang="less" scoped></style> diff --git a/src/tools/ipv4-range-expander/result-row.vue b/src/tools/ipv4-range-expander/result-row.vue index b1782fe..efa0b64 100644 --- a/src/tools/ipv4-range-expander/result-row.vue +++ b/src/tools/ipv4-range-expander/result-row.vue @@ -1,18 +1,6 @@ -<template> - <tr> - <td> - <n-text strong>{{ label }}</n-text> - </td> - <td :data-test-id="testId + '.old'"><span-copyable :value="oldValue" class="monospace" /></td> - <td :data-test-id="testId + '.new'"> - <span-copyable :value="newValue"></span-copyable> - </td> - </tr> -</template> - <script setup lang="ts"> -import SpanCopyable from '@/components/SpanCopyable.vue'; import _ from 'lodash'; +import SpanCopyable from '@/components/SpanCopyable.vue'; const props = withDefaults(defineProps<{ label: string; oldValue?: string; newValue?: string }>(), { label: '', @@ -24,4 +12,18 @@ const { label, oldValue, newValue } = toRefs(props); const testId = computed(() => _.kebabCase(label.value)); </script> -<style scoped lang="less"></style> +<template> + <tr> + <td> + <n-text strong> + {{ label }} + </n-text> + </td> + <td :data-test-id="`${testId}.old`"> + <SpanCopyable :value="oldValue" class="monospace" /> + </td> + <td :data-test-id="`${testId}.new`"> + <SpanCopyable :value="newValue" /> + </td> + </tr> +</template> diff --git a/src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue b/src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue index ca9f424..d16e557 100644 --- a/src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue +++ b/src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue @@ -1,51 +1,12 @@ -<template> - <div> - <c-input-text - v-model:value="ip" - label="An IPv4 address with or without mask" - placeholder="The ipv4 address..." - :validation-rules="ipValidationRules" - mb-4 - /> - - <div v-if="networkInfo"> - <n-table> - <tbody> - <tr v-for="{ getValue, label, undefinedFallback } in sections" :key="label"> - <td> - <n-text strong>{{ label }}</n-text> - </td> - <td> - <span-copyable v-if="getValue(networkInfo)" :value="getValue(networkInfo)"></span-copyable> - <n-text v-else depth="3">{{ undefinedFallback }}</n-text> - </td> - </tr> - </tbody> - </n-table> - - <div mt-3 flex items-center justify-between> - <c-button @click="switchToBlock({ count: -1 })"> - <n-icon :component="ArrowLeft" /> - Previous block - </c-button> - <c-button @click="switchToBlock({ count: 1 })"> - Next block - <n-icon :component="ArrowRight" /> - </c-button> - </div> - </div> - </div> -</template> - <script setup lang="ts"> import { computed } from 'vue'; import { Netmask } from 'netmask'; -import { withDefaultOnError } from '@/utils/defaults'; -import { isNotThrowing } from '@/utils/boolean'; import { useStorage } from '@vueuse/core'; import { ArrowLeft, ArrowRight } from '@vicons/tabler'; -import SpanCopyable from '@/components/SpanCopyable.vue'; import { getIPClass } from './ipv4-subnet-calculator.models'; +import { withDefaultOnError } from '@/utils/defaults'; +import { isNotThrowing } from '@/utils/boolean'; +import SpanCopyable from '@/components/SpanCopyable.vue'; const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24'); @@ -61,13 +22,13 @@ const ipValidationRules = [ ]; const sections: { - label: string; - getValue: (blocks: Netmask) => string | undefined; - undefinedFallback?: string; + label: string + getValue: (blocks: Netmask) => string | undefined + undefinedFallback?: string }[] = [ { label: 'Netmask', - getValue: (block) => block.toString(), + getValue: block => block.toString(), }, { label: 'Network address', @@ -122,4 +83,45 @@ function switchToBlock({ count = 1 }: { count?: number }) { } </script> -<style lang="less" scoped></style> +<template> + <div> + <c-input-text + v-model:value="ip" + label="An IPv4 address with or without mask" + placeholder="The ipv4 address..." + :validation-rules="ipValidationRules" + mb-4 + /> + + <div v-if="networkInfo"> + <n-table> + <tbody> + <tr v-for="{ getValue, label, undefinedFallback } in sections" :key="label"> + <td> + <n-text strong> + {{ label }} + </n-text> + </td> + <td> + <SpanCopyable v-if="getValue(networkInfo)" :value="getValue(networkInfo)" /> + <n-text v-else depth="3"> + {{ undefinedFallback }} + </n-text> + </td> + </tr> + </tbody> + </n-table> + + <div mt-3 flex items-center justify-between> + <c-button @click="switchToBlock({ count: -1 })"> + <n-icon :component="ArrowLeft" /> + Previous block + </c-button> + <c-button @click="switchToBlock({ count: 1 })"> + Next block + <n-icon :component="ArrowRight" /> + </c-button> + </div> + </div> + </div> +</template> diff --git a/src/tools/ipv6-ula-generator/ipv6-ula-generator.vue b/src/tools/ipv6-ula-generator/ipv6-ula-generator.vue index 8f1b130..66c69c6 100644 --- a/src/tools/ipv6-ula-generator/ipv6-ula-generator.vue +++ b/src/tools/ipv6-ula-generator/ipv6-ula-generator.vue @@ -1,36 +1,3 @@ -<template> - <div> - <n-alert title="Info" type="info"> - This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed, - and the lower 40 bits to generate your random ULA. - </n-alert> - - <c-input-text - v-model:value="macAddress" - placeholder="Type a MAC address" - clearable - label="MAC address:" - raw-text - my-8 - :validation="addressValidation" - /> - - <div v-if="addressValidation.isValid"> - <input-copyable - v-for="{ label, value } in calculatedSections" - :key="label" - :value="value" - :label="label" - label-width="160px" - label-align="right" - label-position="left" - readonly - mb-2 - /> - </div> - </div> -</template> - <script setup lang="ts"> import { SHA1 } from 'crypto-js'; import InputCopyable from '@/components/InputCopyable.vue'; @@ -43,7 +10,7 @@ const calculatedSections = computed(() => { .toString() .substring(30); - const ula = 'fd' + hex40bit.substring(0, 2) + ':' + hex40bit.substring(2, 6) + ':' + hex40bit.substring(6); + const ula = `fd${hex40bit.substring(0, 2)}:${hex40bit.substring(2, 6)}:${hex40bit.substring(6)}`; return [ { @@ -64,4 +31,35 @@ const calculatedSections = computed(() => { const addressValidation = macAddressValidation(macAddress); </script> -<style lang="less" scoped></style> +<template> + <div> + <n-alert title="Info" type="info"> + This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed, + and the lower 40 bits to generate your random ULA. + </n-alert> + + <c-input-text + v-model:value="macAddress" + placeholder="Type a MAC address" + clearable + label="MAC address:" + raw-text + my-8 + :validation="addressValidation" + /> + + <div v-if="addressValidation.isValid"> + <InputCopyable + v-for="{ label, value } in calculatedSections" + :key="label" + :value="value" + :label="label" + label-width="160px" + label-align="right" + label-position="left" + readonly + mb-2 + /> + </div> + </div> +</template> diff --git a/src/tools/json-diff/diff-viewer/diff-viewer.models.tsx b/src/tools/json-diff/diff-viewer/diff-viewer.models.tsx index 5a19feb..d2117df 100644 --- a/src/tools/json-diff/diff-viewer/diff-viewer.models.tsx +++ b/src/tools/json-diff/diff-viewer/diff-viewer.models.tsx @@ -1,16 +1,16 @@ import _ from 'lodash'; +import type { ArrayDifference, Difference, ObjectDifference } from '../json-diff.types'; import { useCopy } from '@/composable/copy'; -import type { Difference, ArrayDifference, ObjectDifference } from '../json-diff.types'; -export const DiffRootViewer = ({ diff }: { diff: Difference }) => { +export function DiffRootViewer({ diff }: { diff: Difference }) { return ( <div class={'diffs-viewer'}> <ul>{DiffViewer({ diff, showKeys: false })}</ul> </div> ); -}; +} -const DiffViewer = ({ diff, showKeys = true }: { diff: Difference; showKeys?: boolean }) => { +function DiffViewer({ diff, showKeys = true }: { diff: Difference; showKeys?: boolean }) { const { type, status } = diff; if (status === 'updated') { @@ -26,9 +26,9 @@ const DiffViewer = ({ diff, showKeys = true }: { diff: Difference; showKeys?: bo } return LineDiffViewer({ diff, showKeys }); -}; +} -const LineDiffViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) => { +function LineDiffViewer({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) { const { value, key, status, oldValue } = diff; const valueToDisplay = status === 'removed' ? oldValue : value; @@ -46,9 +46,9 @@ const LineDiffViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boole , </li> ); -}; +} -const ComparisonViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) => { +function ComparisonViewer({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) { const { value, key, oldValue } = diff; return ( @@ -63,21 +63,21 @@ const ComparisonViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boo {Value({ value, status: 'added' })}, </li> ); -}; +} -const ChildrenViewer = ({ +function ChildrenViewer({ diff, openTag, closeTag, showKeys, showChildrenKeys = true, }: { - diff: ArrayDifference | ObjectDifference; - showKeys: boolean; - showChildrenKeys?: boolean; - openTag: string; - closeTag: string; -}) => { + diff: ArrayDifference | ObjectDifference + showKeys: boolean + showChildrenKeys?: boolean + openTag: string + closeTag: string +}) { const { children, key, status, type } = diff; return ( @@ -91,12 +91,12 @@ const ChildrenViewer = ({ )} {openTag} - {children.length > 0 && <ul>{children.map((diff) => DiffViewer({ diff, showKeys: showChildrenKeys }))}</ul>} - {closeTag + ','} + {children.length > 0 && <ul>{children.map(diff => DiffViewer({ diff, showKeys: showChildrenKeys }))}</ul>} + {`${closeTag},`} </div> </li> ); -}; +} function formatValue(value: unknown) { if (_.isNull(value)) { @@ -106,7 +106,7 @@ function formatValue(value: unknown) { return JSON.stringify(value); } -const Value = ({ value, status }: { value: unknown; status: string }) => { +function Value({ value, status }: { value: unknown; status: string }) { const formatedValue = formatValue(value); const { copy } = useCopy({ source: formatedValue }); @@ -116,4 +116,4 @@ const Value = ({ value, status }: { value: unknown; status: string }) => { {formatedValue} </span> ); -}; +} diff --git a/src/tools/json-diff/diff-viewer/diff-viewer.vue b/src/tools/json-diff/diff-viewer/diff-viewer.vue index 8d17e6b..08c4784 100644 --- a/src/tools/json-diff/diff-viewer/diff-viewer.vue +++ b/src/tools/json-diff/diff-viewer/diff-viewer.vue @@ -1,26 +1,11 @@ -<template> - <div v-if="showResults"> - <div flex justify-center> - <n-form-item label="Only show differences" label-placement="left"> - <n-switch v-model:value="onlyShowDifferences" /> - </n-form-item> - </div> - - <c-card data-test-id="diff-result"> - <n-text v-if="jsonAreTheSame" depth="3" block text-center italic> The provided JSONs are the same </n-text> - <diff-root-viewer v-else :diff="result" /> - </c-card> - </div> -</template> - <script lang="ts" setup> -import { useAppTheme } from '@/ui/theme/themes'; import _ from 'lodash'; -import { DiffRootViewer } from './diff-viewer.models'; import { diff } from '../json-diff.models'; +import { DiffRootViewer } from './diff-viewer.models'; +import { useAppTheme } from '@/ui/theme/themes'; -const onlyShowDifferences = ref(false); const props = defineProps<{ leftJson: unknown; rightJson: unknown }>(); +const onlyShowDifferences = ref(false); const { leftJson, rightJson } = toRefs(props); const appTheme = useAppTheme(); @@ -32,6 +17,23 @@ const jsonAreTheSame = computed(() => _.isEqual(leftJson.value, rightJson.value) const showResults = computed(() => !_.isUndefined(leftJson.value) && !_.isUndefined(rightJson.value)); </script> +<template> + <div v-if="showResults"> + <div flex justify-center> + <n-form-item label="Only show differences" label-placement="left"> + <n-switch v-model:value="onlyShowDifferences" /> + </n-form-item> + </div> + + <c-card data-test-id="diff-result"> + <n-text v-if="jsonAreTheSame" depth="3" block text-center italic> + The provided JSONs are the same + </n-text> + <DiffRootViewer v-else :diff="result" /> + </c-card> + </div> +</template> + <style lang="less" scoped> ::v-deep(.diffs-viewer) { color: v-bind('appTheme.text.mutedColor'); diff --git a/src/tools/json-diff/json-diff.e2e.spec.ts b/src/tools/json-diff/json-diff.e2e.spec.ts index 5370060..6bd04f3 100644 --- a/src/tools/json-diff/json-diff.e2e.spec.ts +++ b/src/tools/json-diff/json-diff.e2e.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.describe('Tool - JSON diff', () => { test.beforeEach(async ({ page }) => { @@ -24,7 +24,7 @@ test.describe('Tool - JSON diff', () => { const result = await page.getByTestId('diff-result').innerText(); - expect(result).toContain(`{\nfoo: "bar""buz",\nbaz: "qux",\n},`); + expect(result).toContain('{\nfoo: "bar""buz",\nbaz: "qux",\n},'); }); test('Different JSONs have only differences listed when "Only show differences" is checked', async ({ page }) => { @@ -34,6 +34,6 @@ test.describe('Tool - JSON diff', () => { const result = await page.getByTestId('diff-result').innerText(); - expect(result).toContain(`{\nbaz: "qux",\n},`); + expect(result).toContain('{\nbaz: "qux",\n},'); }); }); diff --git a/src/tools/json-diff/json-diff.models.test.ts b/src/tools/json-diff/json-diff.models.test.ts index b8e699f..fc745f3 100644 --- a/src/tools/json-diff/json-diff.models.test.ts +++ b/src/tools/json-diff/json-diff.models.test.ts @@ -1,4 +1,4 @@ -import { expect, describe, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { diff } from './json-diff.models'; describe('json-diff models', () => { diff --git a/src/tools/json-diff/json-diff.models.ts b/src/tools/json-diff/json-diff.models.ts index ee6ba4b..54bb253 100644 --- a/src/tools/json-diff/json-diff.models.ts +++ b/src/tools/json-diff/json-diff.models.ts @@ -46,8 +46,8 @@ function diffObjects( ): Difference[] { const keys = Object.keys({ ...obj, ...newObj }); return keys - .map((key) => createDifference(obj?.[key], newObj?.[key], key, { onlyShowDifferences })) - .filter((diff) => !onlyShowDifferences || diff.status !== 'unchanged'); + .map(key => createDifference(obj?.[key], newObj?.[key], key, { onlyShowDifferences })) + .filter(diff => !onlyShowDifferences || diff.status !== 'unchanged'); } function createDifference( @@ -99,7 +99,7 @@ function diffArrays( const maxLength = Math.max(0, arr?.length, newArr?.length); return Array.from({ length: maxLength }, (_, i) => createDifference(arr?.[i], newArr?.[i], i, { onlyShowDifferences }), - ).filter((diff) => !onlyShowDifferences || diff.status !== 'unchanged'); + ).filter(diff => !onlyShowDifferences || diff.status !== 'unchanged'); } function getType(value: unknown): 'object' | 'array' | 'value' { diff --git a/src/tools/json-diff/json-diff.types.ts b/src/tools/json-diff/json-diff.types.ts index 8cf58ad..e778f36 100644 --- a/src/tools/json-diff/json-diff.types.ts +++ b/src/tools/json-diff/json-diff.types.ts @@ -1,29 +1,29 @@ export type DifferenceStatus = 'added' | 'removed' | 'updated' | 'unchanged' | 'children-updated'; -export type ObjectDifference = { - key: string | number; - type: 'object'; - children: Difference[]; - status: DifferenceStatus; - oldValue: unknown; - value: unknown; -}; +export interface ObjectDifference { + key: string | number + type: 'object' + children: Difference[] + status: DifferenceStatus + oldValue: unknown + value: unknown +} -export type ValueDifference = { - key: string | number; - type: 'value'; - value: unknown; - oldValue: unknown; - status: DifferenceStatus; -}; +export interface ValueDifference { + key: string | number + type: 'value' + value: unknown + oldValue: unknown + status: DifferenceStatus +} -export type ArrayDifference = { - key: number | string; - type: 'array'; - children: Difference[]; - status: DifferenceStatus; - oldValue: unknown; - value: unknown; -}; +export interface ArrayDifference { + key: number | string + type: 'array' + children: Difference[] + status: DifferenceStatus + oldValue: unknown + value: unknown +} export type Difference = ObjectDifference | ValueDifference | ArrayDifference; diff --git a/src/tools/json-diff/json-diff.vue b/src/tools/json-diff/json-diff.vue index 811f7fa..2ef3de0 100644 --- a/src/tools/json-diff/json-diff.vue +++ b/src/tools/json-diff/json-diff.vue @@ -1,3 +1,24 @@ +<script setup lang="ts"> +import JSON5 from 'json5'; + +import DiffsViewer from './diff-viewer/diff-viewer.vue'; +import { withDefaultOnError } from '@/utils/defaults'; +import { isNotThrowing } from '@/utils/boolean'; + +const rawLeftJson = ref(''); +const rawRightJson = ref(''); + +const leftJson = computed(() => withDefaultOnError(() => JSON5.parse(rawLeftJson.value), undefined)); +const rightJson = computed(() => withDefaultOnError(() => JSON5.parse(rawRightJson.value), undefined)); + +const jsonValidationRules = [ + { + validator: (value: string) => value === '' || isNotThrowing(() => JSON5.parse(value)), + message: 'Invalid JSON format', + }, +]; +</script> + <template> <c-input-text v-model:value="rawLeftJson" @@ -23,24 +44,3 @@ <DiffsViewer :left-json="leftJson" :right-json="rightJson" /> </template> - -<script setup lang="ts"> -import JSON5 from 'json5'; - -import { withDefaultOnError } from '@/utils/defaults'; -import { isNotThrowing } from '@/utils/boolean'; -import DiffsViewer from './diff-viewer/diff-viewer.vue'; - -const rawLeftJson = ref(''); -const rawRightJson = ref(''); - -const leftJson = computed(() => withDefaultOnError(() => JSON5.parse(rawLeftJson.value), undefined)); -const rightJson = computed(() => withDefaultOnError(() => JSON5.parse(rawRightJson.value), undefined)); - -const jsonValidationRules = [ - { - validator: (value: string) => value === '' || isNotThrowing(() => JSON5.parse(value)), - message: 'Invalid JSON format', - }, -]; -</script> diff --git a/src/tools/json-minify/json-minify.vue b/src/tools/json-minify/json-minify.vue index d7e9d15..4ee588d 100644 --- a/src/tools/json-minify/json-minify.vue +++ b/src/tools/json-minify/json-minify.vue @@ -1,19 +1,7 @@ -<template> - <format-transformer - input-label="Your raw json" - :input-default="defaultValue" - input-placeholder="Paste your raw json here..." - output-label="Minify version of your JSON" - output-language="json" - :input-validation-rules="rules" - :transformer="transformer" - /> -</template> - <script setup lang="ts"> +import JSON5 from 'json5'; import type { UseValidationRule } from '@/composable/validation'; import { withDefaultOnError } from '@/utils/defaults'; -import JSON5 from 'json5'; const defaultValue = '{\n\t"hello": [\n\t\t"world"\n\t]\n}'; const transformer = (value: string) => withDefaultOnError(() => JSON.stringify(JSON5.parse(value), null, 0), ''); @@ -25,3 +13,15 @@ const rules: UseValidationRule<string>[] = [ }, ]; </script> + +<template> + <format-transformer + input-label="Your raw json" + :input-default="defaultValue" + input-placeholder="Paste your raw json here..." + output-label="Minify version of your JSON" + output-language="json" + :input-validation-rules="rules" + :transformer="transformer" + /> +</template> diff --git a/src/tools/json-to-yaml-converter/json-to-yaml.e2e.spec.ts b/src/tools/json-to-yaml-converter/json-to-yaml.e2e.spec.ts index bce095b..fee24d9 100644 --- a/src/tools/json-to-yaml-converter/json-to-yaml.e2e.spec.ts +++ b/src/tools/json-to-yaml-converter/json-to-yaml.e2e.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.describe('Tool - json to yaml', () => { test.beforeEach(async ({ page }) => { @@ -14,6 +14,6 @@ test.describe('Tool - json to yaml', () => { const generatedJson = await page.getByTestId('area-content').innerText(); - expect(generatedJson.trim()).toEqual(`foo: bar\nlist:\n - item\n - key: value`.trim()); + expect(generatedJson.trim()).toEqual('foo: bar\nlist:\n - item\n - key: value'.trim()); }); }); diff --git a/src/tools/json-to-yaml-converter/json-to-yaml.vue b/src/tools/json-to-yaml-converter/json-to-yaml.vue index f25ef37..cbaeb22 100644 --- a/src/tools/json-to-yaml-converter/json-to-yaml.vue +++ b/src/tools/json-to-yaml-converter/json-to-yaml.vue @@ -1,20 +1,9 @@ -<template> - <format-transformer - input-label="Your JSON" - input-placeholder="Paste your JSON here..." - output-label="YAML from your JSON" - output-language="yaml" - :input-validation-rules="rules" - :transformer="transformer" - /> -</template> - <script setup lang="ts"> +import { stringify } from 'yaml'; +import JSON5 from 'json5'; import type { UseValidationRule } from '@/composable/validation'; import { isNotThrowing } from '@/utils/boolean'; import { withDefaultOnError } from '@/utils/defaults'; -import { stringify } from 'yaml'; -import JSON5 from 'json5'; const transformer = (value: string) => withDefaultOnError(() => stringify(JSON5.parse(value)), ''); @@ -26,4 +15,13 @@ const rules: UseValidationRule<string>[] = [ ]; </script> -<style lang="less" scoped></style> +<template> + <format-transformer + input-label="Your JSON" + input-placeholder="Paste your JSON here..." + output-label="YAML from your JSON" + output-language="yaml" + :input-validation-rules="rules" + :transformer="transformer" + /> +</template> diff --git a/src/tools/json-viewer/json-viewer.vue b/src/tools/json-viewer/json-viewer.vue index 7e95db1..5fa3d36 100644 --- a/src/tools/json-viewer/json-viewer.vue +++ b/src/tools/json-viewer/json-viewer.vue @@ -1,3 +1,30 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import JSON5 from 'json5'; +import { useStorage } from '@vueuse/core'; +import { formatJson } from './json.models'; +import { withDefaultOnError } from '@/utils/defaults'; +import { useValidation } from '@/composable/validation'; +import TextareaCopyable from '@/components/TextareaCopyable.vue'; + +const inputElement = ref<HTMLElement>(); + +const rawJson = useStorage('json-prettify:raw-json', '{"hello": "world", "foo": "bar"}'); +const indentSize = useStorage('json-prettify:indent-size', 3); +const sortKeys = useStorage('json-prettify:sort-keys', true); +const cleanJson = computed(() => withDefaultOnError(() => formatJson({ rawJson, indentSize, sortKeys }), '')); + +const rawJsonValidation = useValidation({ + source: rawJson, + rules: [ + { + validator: v => v === '' || JSON5.parse(v), + message: 'Provided JSON is not valid.', + }, + ], +}); +</script> + <template> <div style="flex: 0 0 100%"> <div style="margin: 0 auto; max-width: 600px" flex justify-center gap-3> @@ -28,37 +55,10 @@ /> </n-form-item> <n-form-item label="Prettify version of your json"> - <textarea-copyable :value="cleanJson" language="json" :follow-height-of="inputElement" /> + <TextareaCopyable :value="cleanJson" language="json" :follow-height-of="inputElement" /> </n-form-item> </template> -<script setup lang="ts"> -import TextareaCopyable from '@/components/TextareaCopyable.vue'; -import { useValidation } from '@/composable/validation'; -import { withDefaultOnError } from '@/utils/defaults'; -import { computed, ref } from 'vue'; -import JSON5 from 'json5'; -import { useStorage } from '@vueuse/core'; -import { formatJson } from './json.models'; - -const inputElement = ref<HTMLElement>(); - -const rawJson = useStorage('json-prettify:raw-json', '{"hello": "world", "foo": "bar"}'); -const indentSize = useStorage('json-prettify:indent-size', 3); -const sortKeys = useStorage('json-prettify:sort-keys', true); -const cleanJson = computed(() => withDefaultOnError(() => formatJson({ rawJson, indentSize, sortKeys }), '')); - -const rawJsonValidation = useValidation({ - source: rawJson, - rules: [ - { - validator: (v) => v === '' || JSON5.parse(v), - message: 'Provided JSON is not valid.', - }, - ], -}); -</script> - <style lang="less" scoped> .result-card { position: relative; diff --git a/src/tools/json-viewer/json.models.ts b/src/tools/json-viewer/json.models.ts index 10cd326..8d128ca 100644 --- a/src/tools/json-viewer/json.models.ts +++ b/src/tools/json-viewer/json.models.ts @@ -1,4 +1,4 @@ -import { get, type MaybeRef } from '@vueuse/core'; +import { type MaybeRef, get } from '@vueuse/core'; import JSON5 from 'json5'; export { sortObjectKeys, formatJson }; @@ -25,9 +25,9 @@ function formatJson({ sortKeys = true, indentSize = 3, }: { - rawJson: MaybeRef<string>; - sortKeys?: MaybeRef<boolean>; - indentSize?: MaybeRef<number>; + rawJson: MaybeRef<string> + sortKeys?: MaybeRef<boolean> + indentSize?: MaybeRef<number> }) { const parsedObject = JSON5.parse(get(rawJson)); diff --git a/src/tools/jwt-parser/jwt-parser.service.ts b/src/tools/jwt-parser/jwt-parser.service.ts index f5eb04e..19edc5f 100644 --- a/src/tools/jwt-parser/jwt-parser.service.ts +++ b/src/tools/jwt-parser/jwt-parser.service.ts @@ -38,9 +38,11 @@ function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) { .otherwise(() => undefined); } -const dateFormatter = (value: unknown) => { - if (_.isNil(value)) return undefined; +function dateFormatter(value: unknown) { + if (_.isNil(value)) { + return undefined; + } const date = new Date(Number(value) * 1000); return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; -}; +} diff --git a/src/tools/jwt-parser/jwt-parser.vue b/src/tools/jwt-parser/jwt-parser.vue index 7a987b6..f3a460a 100644 --- a/src/tools/jwt-parser/jwt-parser.vue +++ b/src/tools/jwt-parser/jwt-parser.vue @@ -1,35 +1,9 @@ -<template> - <c-card> - <n-form-item label="JWT to decode" :feedback="validation.message" :validation-status="validation.status"> - <n-input v-model:value="rawJwt" type="textarea" placeholder="Put your token here..." rows="5" /> - </n-form-item> - - <n-table v-if="validation.isValid"> - <tbody> - <template v-for="section of sections" :key="section.key"> - <th colspan="2" class="table-header">{{ section.title }}</th> - <tr v-for="{ claim, claimDescription, friendlyValue, value } in decodedJWT[section.key]" :key="claim + value"> - <td class="claims"> - <n-text strong>{{ claim }}</n-text> - <n-text v-if="claimDescription" depth="3" ml-2>({{ claimDescription }})</n-text> - </td> - <td> - <n-text>{{ value }}</n-text> - <n-text v-if="friendlyValue" ml-2 depth="3">({{ friendlyValue }})</n-text> - </td> - </tr> - </template> - </tbody> - </n-table> - </c-card> -</template> - <script setup lang="ts"> +import { computed, ref } from 'vue'; +import { decodeJwt } from './jwt-parser.service'; import { useValidation } from '@/composable/validation'; import { isNotThrowing } from '@/utils/boolean'; import { withDefaultOnError } from '@/utils/defaults'; -import { computed, ref } from 'vue'; -import { decodeJwt } from './jwt-parser.service'; const rawJwt = ref( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', @@ -48,13 +22,47 @@ const validation = useValidation({ source: rawJwt, rules: [ { - validator: (value) => value.length > 0 && isNotThrowing(() => decodeJwt({ jwt: rawJwt.value })), + validator: value => value.length > 0 && isNotThrowing(() => decodeJwt({ jwt: rawJwt.value })), message: 'Invalid JWT', }, ], }); </script> +<template> + <c-card> + <n-form-item label="JWT to decode" :feedback="validation.message" :validation-status="validation.status"> + <n-input v-model:value="rawJwt" type="textarea" placeholder="Put your token here..." rows="5" /> + </n-form-item> + + <n-table v-if="validation.isValid"> + <tbody> + <template v-for="section of sections" :key="section.key"> + <th colspan="2" class="table-header"> + {{ section.title }} + </th> + <tr v-for="{ claim, claimDescription, friendlyValue, value } in decodedJWT[section.key]" :key="claim + value"> + <td class="claims"> + <n-text strong> + {{ claim }} + </n-text> + <n-text v-if="claimDescription" depth="3" ml-2> + ({{ claimDescription }}) + </n-text> + </td> + <td> + <n-text>{{ value }}</n-text> + <n-text v-if="friendlyValue" ml-2 depth="3"> + ({{ friendlyValue }}) + </n-text> + </td> + </tr> + </template> + </tbody> + </n-table> + </c-card> +</template> + <style lang="less" scoped> .table-header { text-align: center; diff --git a/src/tools/keycode-info/keycode-info.vue b/src/tools/keycode-info/keycode-info.vue index bc11667..132fc98 100644 --- a/src/tools/keycode-info/keycode-info.vue +++ b/src/tools/keycode-info/keycode-info.vue @@ -1,17 +1,3 @@ -<template> - <div> - <c-card style="text-align: center; padding: 40px 0; margin-bottom: 26px"> - <n-h2 v-if="event">{{ event.key }}</n-h2> - <n-text strong depth="3">Press the key on your keyboard you want to get info about this key</n-text> - </c-card> - - <n-input-group v-for="({ label, value, placeholder }, i) of fields" :key="i" style="margin-bottom: 5px"> - <n-input-group-label style="flex: 0 0 150px"> {{ label }} </n-input-group-label> - <input-copyable :value="value" readonly :placeholder="placeholder" /> - </n-input-group> - </div> -</template> - <script setup lang="ts"> import { useEventListener } from '@vueuse/core'; import { computed, ref } from 'vue'; @@ -24,7 +10,9 @@ useEventListener(document, 'keydown', (e) => { }); const fields = computed(() => { - if (!event.value) return []; + if (!event.value) { + return []; + } return [ { @@ -64,4 +52,22 @@ const fields = computed(() => { }); </script> -<style lang="less" scoped></style> +<template> + <div> + <c-card style="text-align: center; padding: 40px 0; margin-bottom: 26px"> + <n-h2 v-if="event"> + {{ event.key }} + </n-h2> + <n-text strong depth="3"> + Press the key on your keyboard you want to get info about this key + </n-text> + </c-card> + + <n-input-group v-for="({ label, value, placeholder }, i) of fields" :key="i" style="margin-bottom: 5px"> + <n-input-group-label style="flex: 0 0 150px"> + {{ label }} + </n-input-group-label> + <InputCopyable :value="value" readonly :placeholder="placeholder" /> + </n-input-group> + </div> +</template> diff --git a/src/tools/list-converter/list-converter.e2e.spec.ts b/src/tools/list-converter/list-converter.e2e.spec.ts index a5ac8c6..4accbf0 100644 --- a/src/tools/list-converter/list-converter.e2e.spec.ts +++ b/src/tools/list-converter/list-converter.e2e.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.describe('Tool - List converter', () => { test.beforeEach(async ({ page }) => { @@ -30,10 +30,10 @@ test.describe('Tool - List converter', () => { 3 5`); await page.getByTestId('removeDuplicates').check(); - await page.getByTestId('itemPrefix').fill("'"); - await page.getByTestId('itemSuffix').fill("'"); + await page.getByTestId('itemPrefix').fill('\''); + await page.getByTestId('itemSuffix').fill('\''); const result = await page.getByTestId('area-content').innerText(); - expect(result.trim()).toEqual("'1', '2', '4', '3', '5'"); + expect(result.trim()).toEqual('\'1\', \'2\', \'4\', \'3\', \'5\''); }); }); diff --git a/src/tools/list-converter/list-converter.models.test.ts b/src/tools/list-converter/list-converter.models.test.ts index 944f0f3..abbc43c 100644 --- a/src/tools/list-converter/list-converter.models.test.ts +++ b/src/tools/list-converter/list-converter.models.test.ts @@ -1,4 +1,4 @@ -import { expect, describe, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { convert } from './list-converter.models'; import type { ConvertOptions } from './list-converter.types'; diff --git a/src/tools/list-converter/list-converter.models.ts b/src/tools/list-converter/list-converter.models.ts index 1df9b38..548baa2 100644 --- a/src/tools/list-converter/list-converter.models.ts +++ b/src/tools/list-converter/list-converter.models.ts @@ -1,27 +1,27 @@ import _ from 'lodash'; -import { byOrder } from '@/utils/array'; import type { ConvertOptions } from './list-converter.types'; +import { byOrder } from '@/utils/array'; export { convert }; -const whenever = - <T, R>(condition: boolean, fn: (value: T) => R) => - (value: T) => +function whenever<T, R>(condition: boolean, fn: (value: T) => R) { + return (value: T) => condition ? fn(value) : value; +} function convert(list: string, options: ConvertOptions): string { const lineBreak = options.keepLineBreaks ? '\n' : ''; return _.chain(list) - .thru(whenever(options.lowerCase, (text) => text.toLowerCase())) + .thru(whenever(options.lowerCase, text => text.toLowerCase())) .split('\n') .thru(whenever(options.removeDuplicates, _.uniq)) .thru(whenever(options.reverseList, _.reverse)) - .thru(whenever(!_.isNull(options.sortList), (parts) => parts.sort(byOrder({ order: options.sortList })))) + .thru(whenever(!_.isNull(options.sortList), parts => parts.sort(byOrder({ order: options.sortList })))) .map(whenever(options.trimItems, _.trim)) .without('') - .map((p) => options.itemPrefix + p + options.itemSuffix) + .map(p => options.itemPrefix + p + options.itemSuffix) .join(options.separator + lineBreak) - .thru((text) => [options.listPrefix, text, options.listSuffix].join(lineBreak)) + .thru(text => [options.listPrefix, text, options.listSuffix].join(lineBreak)) .value(); } diff --git a/src/tools/list-converter/list-converter.types.ts b/src/tools/list-converter/list-converter.types.ts index cfcaead..3f9ea3f 100644 --- a/src/tools/list-converter/list-converter.types.ts +++ b/src/tools/list-converter/list-converter.types.ts @@ -1,15 +1,15 @@ export type SortOrder = 'asc' | 'desc' | null; -export type ConvertOptions = { - lowerCase: boolean; - trimItems: boolean; - itemPrefix: string; - itemSuffix: string; - listPrefix: string; - listSuffix: string; - reverseList: boolean; - sortList: SortOrder; - removeDuplicates: boolean; - separator: string; - keepLineBreaks: boolean; -}; +export interface ConvertOptions { + lowerCase: boolean + trimItems: boolean + itemPrefix: string + itemSuffix: string + listPrefix: string + listSuffix: string + reverseList: boolean + sortList: SortOrder + removeDuplicates: boolean + separator: string + keepLineBreaks: boolean +} diff --git a/src/tools/list-converter/list-converter.vue b/src/tools/list-converter/list-converter.vue index 7258dc3..2e6d2b3 100644 --- a/src/tools/list-converter/list-converter.vue +++ b/src/tools/list-converter/list-converter.vue @@ -1,3 +1,40 @@ +<script setup lang="ts"> +import { useStorage } from '@vueuse/core'; +import { convert } from './list-converter.models'; +import type { ConvertOptions } from './list-converter.types'; + +const sortOrderOptions = [ + { + label: 'Sort ascending', + value: 'asc', + disabled: false, + }, + { + label: 'Sort descending', + value: 'desc', + disabled: false, + }, +]; + +const conversionConfig = useStorage<ConvertOptions>('list-converter:conversionConfig', { + lowerCase: false, + trimItems: true, + removeDuplicates: true, + keepLineBreaks: false, + itemPrefix: '', + itemSuffix: '', + listPrefix: '', + listSuffix: '', + reverseList: false, + sortList: null, + separator: ', ', +}); + +function transformer(value: string) { + return convert(value, conversionConfig.value); +} +</script> + <template> <div style="flex: 0 0 100%"> <div style="margin: 0 auto; max-width: 600px"> @@ -82,42 +119,3 @@ :transformer="transformer" /> </template> - -<script setup lang="ts"> -import { useStorage } from '@vueuse/core'; -import { convert } from './list-converter.models'; -import type { ConvertOptions } from './list-converter.types'; - -const sortOrderOptions = [ - { - label: 'Sort ascending', - value: 'asc', - disabled: false, - }, - { - label: 'Sort descending', - value: 'desc', - disabled: false, - }, -]; - -const conversionConfig = useStorage<ConvertOptions>('list-converter:conversionConfig', { - lowerCase: false, - trimItems: true, - removeDuplicates: true, - keepLineBreaks: false, - itemPrefix: '', - itemSuffix: '', - listPrefix: '', - listSuffix: '', - reverseList: false, - sortList: null, - separator: ', ', -}); - -const transformer = (value: string) => { - return convert(value, conversionConfig.value); -}; -</script> - -<style lang="less" scoped></style> diff --git a/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.service.ts b/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.service.ts index 4122a7b..b21a302 100644 --- a/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.service.ts +++ b/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.service.ts @@ -179,13 +179,13 @@ const vocabulary = [ ]; const firstSentence = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; -const generateSentence = (length: number) => { +function generateSentence(length: number) { const sentence = Array.from({ length }) .map(() => randFromArray(vocabulary)) .join(' '); - return sentence.charAt(0).toUpperCase() + sentence.slice(1) + '.'; -}; + return `${sentence.charAt(0).toUpperCase() + sentence.slice(1)}.`; +} export function generateLoremIpsum({ paragraphCount = 1, @@ -194,11 +194,11 @@ export function generateLoremIpsum({ startWithLoremIpsum = true, asHTML = false, }: { - paragraphCount?: number; - sentencePerParagraph?: number; - wordCount?: number; - startWithLoremIpsum?: boolean; - asHTML?: boolean; + paragraphCount?: number + sentencePerParagraph?: number + wordCount?: number + startWithLoremIpsum?: boolean + asHTML?: boolean }) { const paragraphs = Array.from({ length: paragraphCount }).map(() => Array.from({ length: sentencePerParagraph }).map(() => generateSentence(wordCount)), @@ -209,8 +209,8 @@ export function generateLoremIpsum({ } if (asHTML) { - return `<p>${paragraphs.map((s) => s.join(' ')).join('</p>\n\n<p>')}</p>`; + return `<p>${paragraphs.map(s => s.join(' ')).join('</p>\n\n<p>')}</p>`; } - return paragraphs.map((s) => s.join(' ')).join('\n\n'); + return paragraphs.map(s => s.join(' ')).join('\n\n'); } diff --git a/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue b/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue index 9e3b186..4423eb4 100644 --- a/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue +++ b/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue @@ -1,3 +1,27 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import { generateLoremIpsum } from './lorem-ipsum-generator.service'; +import { useCopy } from '@/composable/copy'; +import { randIntFromInterval } from '@/utils/random'; + +const paragraphs = ref(1); +const sentences = ref([3, 8]); +const words = ref([8, 15]); +const startWithLoremIpsum = ref(true); +const asHTML = ref(false); + +const loremIpsumText = computed(() => + generateLoremIpsum({ + paragraphCount: paragraphs.value, + asHTML: asHTML.value, + sentencePerParagraph: randIntFromInterval(sentences.value[0], sentences.value[1]), + wordCount: randIntFromInterval(words.value[0], words.value[1]), + startWithLoremIpsum: startWithLoremIpsum.value, + }), +); +const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to the clipboard' }); +</script> + <template> <c-card> <n-form-item label="Paragraphs" :show-feedback="false" label-width="200" label-placement="left"> @@ -19,31 +43,9 @@ <n-input :value="loremIpsumText" type="textarea" placeholder="Your lorem ipsum..." readonly autosize mt-5 /> <div mt-5 flex justify-center> - <c-button autofocus @click="copy"> Copy </c-button> + <c-button autofocus @click="copy"> + Copy + </c-button> </div> </c-card> </template> - -<script setup lang="ts"> -import { useCopy } from '@/composable/copy'; -import { ref, computed } from 'vue'; -import { randIntFromInterval } from '@/utils/random'; -import { generateLoremIpsum } from './lorem-ipsum-generator.service'; - -const paragraphs = ref(1); -const sentences = ref([3, 8]); -const words = ref([8, 15]); -const startWithLoremIpsum = ref(true); -const asHTML = ref(false); - -const loremIpsumText = computed(() => - generateLoremIpsum({ - paragraphCount: paragraphs.value, - asHTML: asHTML.value, - sentencePerParagraph: randIntFromInterval(sentences.value[0], sentences.value[1]), - wordCount: randIntFromInterval(words.value[0], words.value[1]), - startWithLoremIpsum: startWithLoremIpsum.value, - }), -); -const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to the clipboard' }); -</script> diff --git a/src/tools/mac-address-lookup/mac-address-lookup.vue b/src/tools/mac-address-lookup/mac-address-lookup.vue index 095a0ee..05382eb 100644 --- a/src/tools/mac-address-lookup/mac-address-lookup.vue +++ b/src/tools/mac-address-lookup/mac-address-lookup.vue @@ -1,3 +1,16 @@ +<script setup lang="ts"> +import db from 'oui/oui.json'; +import { macAddressValidationRules } from '@/utils/macAddress'; +import { useCopy } from '@/composable/copy'; + +const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '').toUpperCase().substring(0, 6); + +const macAddress = ref('20:37:06:12:34:56'); +const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]); + +const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' }); +</script> + <template> <div> <c-input-text @@ -14,32 +27,25 @@ mb-5 /> - <div mb-5px>Vendor info:</div> + <div mb-5px> + Vendor info: + </div> <c-card mb-5> <div v-if="details"> - <div v-for="(detail, index) of details.split('\n')" :key="index">{{ detail }}</div> + <div v-for="(detail, index) of details.split('\n')" :key="index"> + {{ detail }} + </div> </div> - <div v-else italic op-60>Unknown vendor for this address</div> + <div v-else italic op-60> + Unknown vendor for this address + </div> </c-card> <div flex justify-center> - <c-button :disabled="!details" @click="copy"> Copy vendor info </c-button> + <c-button :disabled="!details" @click="copy"> + Copy vendor info + </c-button> </div> </div> </template> - -<script setup lang="ts"> -import db from 'oui/oui.json'; -import { macAddressValidationRules } from '@/utils/macAddress'; -import { useCopy } from '@/composable/copy'; - -const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '').toUpperCase().substring(0, 6); - -const macAddress = ref('20:37:06:12:34:56'); -const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]); - -const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' }); -</script> - -<style lang="less" scoped></style> diff --git a/src/tools/math-evaluator/index.ts b/src/tools/math-evaluator/index.ts index ad783d2..3dc15f4 100644 --- a/src/tools/math-evaluator/index.ts +++ b/src/tools/math-evaluator/index.ts @@ -4,7 +4,7 @@ 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: 'Evaluate math expression, like a calculator on steroid (you can use function like sqrt, cos, sin, abs, ...)', keywords: [ 'math', 'evaluator', diff --git a/src/tools/math-evaluator/math-evaluator.vue b/src/tools/math-evaluator/math-evaluator.vue index a8b359b..0429e6e 100644 --- a/src/tools/math-evaluator/math-evaluator.vue +++ b/src/tools/math-evaluator/math-evaluator.vue @@ -1,3 +1,13 @@ +<script setup lang="ts"> +import { evaluate } from 'mathjs'; +import { computed, ref } from 'vue'; +import { withDefaultOnError } from '@/utils/defaults'; + +const expression = ref(''); + +const result = computed(() => withDefaultOnError(() => evaluate(expression.value) ?? '', '')); +</script> + <template> <div> <n-input @@ -17,13 +27,3 @@ </c-card> </div> </template> - -<script setup lang="ts"> -import { withDefaultOnError } from '@/utils/defaults'; -import { evaluate } from 'mathjs'; -import { computed, ref } from 'vue'; - -const expression = ref(''); - -const result = computed(() => withDefaultOnError(() => evaluate(expression.value) ?? '', '')); -</script> diff --git a/src/tools/meta-tag-generator/OGSchemaType.type.ts b/src/tools/meta-tag-generator/OGSchemaType.type.ts index 8d09013..64387b8 100644 --- a/src/tools/meta-tag-generator/OGSchemaType.type.ts +++ b/src/tools/meta-tag-generator/OGSchemaType.type.ts @@ -3,25 +3,25 @@ import type { SelectGroupOption, SelectOption } from 'naive-ui'; export type { OGSchemaType, OGSchemaTypeElementInput, OGSchemaTypeElementSelect, OGSchemaTypeElementInputMultiple }; interface OGSchemaTypeElementBase { - key: string; - label: string; - placeholder: string; + key: string + label: string + placeholder: string } interface OGSchemaTypeElementInput extends OGSchemaTypeElementBase { - type: 'input'; + type: 'input' } interface OGSchemaTypeElementInputMultiple extends OGSchemaTypeElementBase { - type: 'input-multiple'; + type: 'input-multiple' } interface OGSchemaTypeElementSelect extends OGSchemaTypeElementBase { - type: 'select'; - options: Array<SelectOption | SelectGroupOption>; + type: 'select' + options: Array<SelectOption | SelectGroupOption> } interface OGSchemaType { - name: string; - elements: (OGSchemaTypeElementSelect | OGSchemaTypeElementInput | OGSchemaTypeElementInputMultiple)[]; + name: string + elements: (OGSchemaTypeElementSelect | OGSchemaTypeElementInput | OGSchemaTypeElementInputMultiple)[] } diff --git a/src/tools/meta-tag-generator/meta-tag-generator.vue b/src/tools/meta-tag-generator/meta-tag-generator.vue index 73fad8b..12e660e 100644 --- a/src/tools/meta-tag-generator/meta-tag-generator.vue +++ b/src/tools/meta-tag-generator/meta-tag-generator.vue @@ -1,48 +1,15 @@ -<template> - <div> - <div v-for="{ name, elements } of sections" :key="name" style="margin-bottom: 15px"> - <n-form-item :label="name" :show-feedback="false"> </n-form-item> - - <n-input-group v-for="{ key, type, label, placeholder, ...element } of elements" :key="key"> - <n-input-group-label style="flex: 0 0 110px">{{ label }}</n-input-group-label> - <c-input-text v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" clearable /> - <n-dynamic-input - v-else-if="type === 'input-multiple'" - v-model:value="metadata[key]" - :min="1" - :placeholder="placeholder" - :default-value="['']" - :show-sort-button="true" - /> - - <n-select - v-else-if="type === 'select'" - v-model:value="metadata[key]" - :placeholder="placeholder" - :options="(element as OGSchemaTypeElementSelect).options" - /> - </n-input-group> - </div> - </div> - <div> - <n-form-item label="Your meta tags"> - <textarea-copyable :value="metaTags" language="html" /> - </n-form-item> - </div> -</template> - <script setup lang="ts"> -import TextareaCopyable from '@/components/TextareaCopyable.vue'; import { generateMeta } from '@it-tools/oggen'; import _ from 'lodash'; import { computed, ref, watch } from 'vue'; import { image, ogSchemas, twitter, website } from './og-schemas'; import type { OGSchemaType, OGSchemaTypeElementSelect } from './OGSchemaType.type'; +import TextareaCopyable from '@/components/TextareaCopyable.vue'; // Since type guards do not work in template -// eslint-disable-next-line @typescript-eslint/no-explicit-any + const metadata = ref<{ type: string; [k: string]: any }>({ - type: 'website', + 'type': 'website', 'twitter:card': 'summary_large_image', }); @@ -51,7 +18,9 @@ watch( (_ignored, prevSection) => { const section = ogSchemas[prevSection.value]; - if (!section) return; + if (!section) { + return; + } section.elements.forEach(({ key }) => { metadata.value[key] = ''; @@ -63,7 +32,9 @@ const sections = computed(() => { const secs: OGSchemaType[] = [website, image, twitter]; const additionalSchema = ogSchemas[metadata.value.type]; - if (additionalSchema) secs.push(additionalSchema); + if (additionalSchema) { + secs.push(additionalSchema); + } return secs; }); @@ -80,6 +51,41 @@ const metaTags = computed(() => { }); </script> +<template> + <div> + <div v-for="{ name, elements } of sections" :key="name" style="margin-bottom: 15px"> + <n-form-item :label="name" :show-feedback="false" /> + + <n-input-group v-for="{ key, type, label, placeholder, ...element } of elements" :key="key"> + <n-input-group-label style="flex: 0 0 110px"> + {{ label }} + </n-input-group-label> + <c-input-text v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" clearable /> + <n-dynamic-input + v-else-if="type === 'input-multiple'" + v-model:value="metadata[key]" + :min="1" + :placeholder="placeholder" + :default-value="['']" + :show-sort-button="true" + /> + + <n-select + v-else-if="type === 'select'" + v-model:value="metadata[key]" + :placeholder="placeholder" + :options="(element as OGSchemaTypeElementSelect).options" + /> + </n-input-group> + </div> + </div> + <div> + <n-form-item label="Your meta tags"> + <TextareaCopyable :value="metaTags" language="html" /> + </n-form-item> + </div> +</template> + <style lang="less" scoped> .n-input-group { margin-bottom: 5px; diff --git a/src/tools/meta-tag-generator/og-schemas/videoMovie.ts b/src/tools/meta-tag-generator/og-schemas/videoMovie.ts index 6d5b02d..96baa1a 100644 --- a/src/tools/meta-tag-generator/og-schemas/videoMovie.ts +++ b/src/tools/meta-tag-generator/og-schemas/videoMovie.ts @@ -17,7 +17,7 @@ export const videoMovie: OGSchemaType = { placeholder: 'Name of the director...', }, { type: 'input-multiple', label: 'Writer', key: 'video:writer', placeholder: 'Writers of the movie...' }, - { type: 'input', label: 'Duration', key: 'video:duration', placeholder: "The movie's length in seconds..." }, + { type: 'input', label: 'Duration', key: 'video:duration', placeholder: 'The movie\'s length in seconds...' }, { type: 'input', label: 'Release date', diff --git a/src/tools/mime-types/mime-types.vue b/src/tools/mime-types/mime-types.vue index f19c183..f67475b 100644 --- a/src/tools/mime-types/mime-types.vue +++ b/src/tools/mime-types/mime-types.vue @@ -1,7 +1,32 @@ +<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> + <template> <c-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-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" @@ -13,7 +38,9 @@ </n-form-item> <div v-if="extensionsFound.length > 0"> - Extensions of files with the <n-tag round :bordered="false">{{ selectedMimeType }}</n-tag> mime-type: + 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" @@ -30,8 +57,12 @@ </c-card> <c-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-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" @@ -43,7 +74,9 @@ </n-form-item> <div v-if="selectedExtension"> - Mime type associated to the extension <n-tag round :bordered="false">{{ selectedExtension }}</n-tag> file + 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"> @@ -74,24 +107,3 @@ </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> diff --git a/src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue b/src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue index 95527d0..f3534eb 100644 --- a/src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue +++ b/src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue @@ -1,3 +1,57 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import { useTimestamp } from '@vueuse/core'; +import { useThemeVars } from 'naive-ui'; +import { useQRCode } from '../qr-code-generator/useQRCode'; +import { base32toHex, buildKeyUri, generateSecret, generateTOTP, getCounterFromTime } from './otp.service'; +import TokenDisplay from './token-display.vue'; +import { useStyleStore } from '@/stores/style.store'; +import InputCopyable from '@/components/InputCopyable.vue'; +import { computedRefreshable } from '@/composable/computedRefreshable'; + +const now = useTimestamp(); +const interval = computed(() => (now.value / 1000) % 30); +const theme = useThemeVars(); +const styleStore = useStyleStore(); + +const secret = ref(generateSecret()); + +function refreshSecret() { + secret.value = generateSecret(); +} + +const [tokens] = computedRefreshable( + () => ({ + previous: generateTOTP({ key: secret.value, now: now.value - 30000 }), + current: generateTOTP({ key: secret.value, now: now.value }), + next: generateTOTP({ key: secret.value, now: now.value + 30000 }), + }), + { throttle: 500 }, +); + +const keyUri = computed(() => buildKeyUri({ secret: secret.value })); + +const { qrcode } = useQRCode({ + text: keyUri, + color: { + background: computed(() => (styleStore.isDarkTheme ? '#ffffff' : '#00000000')), + foreground: '#000000', + }, + options: { width: 210 }, +}); + +const secretValidationRules = [ + { + message: 'Secret should be a base32 string', + validator: (value: string) => value.toUpperCase().match(/^[A-Z234567]+$/), + }, + { + message: 'Please set a secret', + validator: (value: string) => value !== '', + }, +]; +</script> + <template> <div style="max-width: 350px"> <c-input-text @@ -20,18 +74,22 @@ </c-input-text> <div> - <token-display :tokens="tokens" style="margin-top: 2px" /> + <TokenDisplay :tokens="tokens" style="margin-top: 2px" /> <n-progress :percentage="(100 * interval) / 30" :color="theme.primaryColor" :show-indicator="false" /> - <div style="text-align: center">Next in {{ String(Math.floor(30 - interval)).padStart(2, '0') }}s</div> + <div style="text-align: center"> + Next in {{ String(Math.floor(30 - interval)).padStart(2, '0') }}s + </div> </div> <div mt-4 flex flex-col items-center justify-center gap-3> - <n-image :src="qrcode"></n-image> - <c-button :href="keyUri" target="_blank">Open Key URI in new tab</c-button> + <n-image :src="qrcode" /> + <c-button :href="keyUri" target="_blank"> + Open Key URI in new tab + </c-button> </div> </div> <div style="max-width: 350px"> - <input-copyable + <InputCopyable label="Secret in hexadecimal" :value="base32toHex(secret)" readonly @@ -39,7 +97,7 @@ mb-5 /> - <input-copyable + <InputCopyable label="Epoch" :value="Math.floor(now / 1000).toString()" readonly @@ -49,7 +107,7 @@ <p>Iteration</p> - <input-copyable + <InputCopyable :value="String(getCounterFromTime({ now, timeStep: 30 }))" readonly label="Count:" @@ -59,7 +117,7 @@ placeholder="Iteration count will be displayed here" /> - <input-copyable + <InputCopyable :value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')" readonly placeholder="Iteration count in hex will be displayed here" @@ -71,60 +129,6 @@ </div> </template> -<script setup lang="ts"> -import { computed, ref } from 'vue'; -import { useTimestamp } from '@vueuse/core'; -import { useThemeVars } from 'naive-ui'; -import { useStyleStore } from '@/stores/style.store'; -import InputCopyable from '@/components/InputCopyable.vue'; -import { computedRefreshable } from '@/composable/computedRefreshable'; -import { generateTOTP, buildKeyUri, generateSecret, base32toHex, getCounterFromTime } from './otp.service'; -import { useQRCode } from '../qr-code-generator/useQRCode'; -import TokenDisplay from './token-display.vue'; - -const now = useTimestamp(); -const interval = computed(() => (now.value / 1000) % 30); -const theme = useThemeVars(); -const styleStore = useStyleStore(); - -const secret = ref(generateSecret()); - -function refreshSecret() { - secret.value = generateSecret(); -} - -const [tokens] = computedRefreshable( - () => ({ - previous: generateTOTP({ key: secret.value, now: now.value - 30000 }), - current: generateTOTP({ key: secret.value, now: now.value }), - next: generateTOTP({ key: secret.value, now: now.value + 30000 }), - }), - { throttle: 500 }, -); - -const keyUri = computed(() => buildKeyUri({ secret: secret.value })); - -const { qrcode } = useQRCode({ - text: keyUri, - color: { - background: computed(() => (styleStore.isDarkTheme ? '#ffffff' : '#00000000')), - foreground: '#000000', - }, - options: { width: 210 }, -}); - -const secretValidationRules = [ - { - message: 'Secret should be a base32 string', - validator: (value: string) => value.toUpperCase().match(/^[A-Z234567]+$/), - }, - { - message: 'Please set a secret', - validator: (value: string) => value !== '', - }, -]; -</script> - <style lang="less" scoped> .n-progress { margin-top: 10px; diff --git a/src/tools/otp-code-generator-and-validator/otp-code-generator.e2e.spec.ts b/src/tools/otp-code-generator-and-validator/otp-code-generator.e2e.spec.ts index 6188f82..40fb426 100644 --- a/src/tools/otp-code-generator-and-validator/otp-code-generator.e2e.spec.ts +++ b/src/tools/otp-code-generator-and-validator/otp-code-generator.e2e.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.describe('Tool - OTP code generator', () => { test.beforeEach(async ({ page }) => { @@ -19,7 +19,7 @@ test.describe('Tool - OTP code generator', () => { test('OTP a generated from the provided secret', async ({ page }) => { page.evaluate(() => { - Date.now = () => 1609477200000; //Jan 1, 2021 + Date.now = () => 1609477200000; // Jan 1, 2021 }); await page.getByPlaceholder('Paste your TOTP secret...').fill('ITTOOLS'); diff --git a/src/tools/otp-code-generator-and-validator/otp.service.test.ts b/src/tools/otp-code-generator-and-validator/otp.service.test.ts index 24402db..2ba1836 100644 --- a/src/tools/otp-code-generator-and-validator/otp.service.test.ts +++ b/src/tools/otp-code-generator-and-validator/otp.service.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from 'vitest'; import { + base32toHex, + buildKeyUri, generateHOTP, + generateTOTP, hexToBytes, verifyHOTP, - generateTOTP, verifyTOTP, - buildKeyUri, - base32toHex, } from './otp.service'; describe('otp functions', () => { diff --git a/src/tools/otp-code-generator-and-validator/otp.service.ts b/src/tools/otp-code-generator-and-validator/otp.service.ts index f1cb65e..fae3d37 100644 --- a/src/tools/otp-code-generator-and-validator/otp.service.ts +++ b/src/tools/otp-code-generator-and-validator/otp.service.ts @@ -1,4 +1,4 @@ -import { enc, HmacSHA1 } from 'crypto-js'; +import { HmacSHA1, enc } from 'crypto-js'; import _ from 'lodash'; import { createToken } from '../token-generator/token-generator.service'; @@ -15,7 +15,7 @@ export { }; function hexToBytes(hex: string) { - return (hex.match(/.{1,2}/g) ?? []).map((char) => parseInt(char, 16)); + return (hex.match(/.{1,2}/g) ?? []).map(char => parseInt(char, 16)); } function computeHMACSha1(message: string, key: string) { @@ -29,10 +29,10 @@ function base32toHex(base32: string) { .toUpperCase() // Since base 32, we coerce lowercase to uppercase .replace(/=+$/, '') .split('') - .map((value) => base32Chars.indexOf(value).toString(2).padStart(5, '0')) + .map(value => base32Chars.indexOf(value).toString(2).padStart(5, '0')) .join(''); - const hex = (bits.match(/.{1,8}/g) ?? []).map((chunk) => parseInt(chunk, 2).toString(16).padStart(2, '0')).join(''); + const hex = (bits.match(/.{1,8}/g) ?? []).map(chunk => parseInt(chunk, 2).toString(16).padStart(2, '0')).join(''); return hex; } @@ -45,12 +45,12 @@ function generateHOTP({ key, counter = 0 }: { key: string; counter?: number }) { const bytes = hexToBytes(digest); // Truncate - const offset = bytes[19] & 0xf; - const v = - ((bytes[offset] & 0x7f) << 24) | - ((bytes[offset + 1] & 0xff) << 16) | - ((bytes[offset + 2] & 0xff) << 8) | - (bytes[offset + 3] & 0xff); + const offset = bytes[19] & 0xF; + const v + = ((bytes[offset] & 0x7F) << 24) + | ((bytes[offset + 1] & 0xFF) << 16) + | ((bytes[offset + 2] & 0xFF) << 8) + | (bytes[offset + 3] & 0xFF); const code = String(v % 1000000).padStart(6, '0'); @@ -63,10 +63,10 @@ function verifyHOTP({ window = 0, counter = 0, }: { - token: string; - key: string; - window?: number; - counter?: number; + token: string + key: string + window?: number + counter?: number }) { for (let i = counter - window; i <= counter + window; ++i) { if (generateHOTP({ key, counter: i }) === token) { @@ -94,11 +94,11 @@ function verifyTOTP({ now = Date.now(), timeStep = 30, }: { - token: string; - key: string; - window?: number; - now?: number; - timeStep?: number; + token: string + key: string + window?: number + now?: number + timeStep?: number }) { const counter = getCounterFromTime({ now, timeStep }); @@ -113,12 +113,12 @@ function buildKeyUri({ digits = 6, period = 30, }: { - secret: string; - app?: string; - account?: string; - algorithm?: string; - digits?: number; - period?: number; + secret: string + app?: string + account?: string + algorithm?: string + digits?: number + period?: number }) { const params = { issuer: app, diff --git a/src/tools/otp-code-generator-and-validator/token-display.vue b/src/tools/otp-code-generator-and-validator/token-display.vue index 532cbe7..34ceb0b 100644 --- a/src/tools/otp-code-generator-and-validator/token-display.vue +++ b/src/tools/otp-code-generator-and-validator/token-display.vue @@ -1,9 +1,27 @@ +<script setup lang="ts"> +import { useClipboard } from '@vueuse/core'; +import { toRefs } from 'vue'; + +const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>(); +const { copy: copyPrevious, copied: previousCopied } = useClipboard(); +const { copy: copyCurrent, copied: currentCopied } = useClipboard(); +const { copy: copyNext, copied: nextCopied } = useClipboard(); + +const { tokens } = toRefs(props); +</script> + <template> <div> <div class="labels" w-full flex items-center> - <div flex-1 text-left>Previous</div> - <div flex-1 text-center>Current OTP</div> - <div flex-1 text-right>Next</div> + <div flex-1 text-left> + Previous + </div> + <div flex-1 text-center> + Current OTP + </div> + <div flex-1 text-right> + Next + </div> </div> <n-input-group> <n-tooltip trigger="hover" placement="bottom"> @@ -29,9 +47,11 @@ </n-tooltip> <n-tooltip trigger="hover" placement="bottom"> <template #trigger> - <c-button important:h-12 data-test-id="next-otp" @click.prevent="copyNext(tokens.next)">{{ - tokens.next - }}</c-button> + <c-button important:h-12 data-test-id="next-otp" @click.prevent="copyNext(tokens.next)"> + {{ + tokens.next + }} + </c-button> </template> <div>{{ nextCopied ? 'Copied !' : 'Copy next OTP' }}</div> </n-tooltip> @@ -39,18 +59,6 @@ </div> </template> -<script setup lang="ts"> -import { useClipboard } from '@vueuse/core'; -import { toRefs } from 'vue'; - -const { copy: copyPrevious, copied: previousCopied } = useClipboard(); -const { copy: copyCurrent, copied: currentCopied } = useClipboard(); -const { copy: copyNext, copied: nextCopied } = useClipboard(); - -const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>(); -const { tokens } = toRefs(props); -</script> - <style scoped lang="less"> .current-otp { font-size: 22px; diff --git a/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.e2e.spec.ts b/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.e2e.spec.ts index ddf5510..68a2334 100644 --- a/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.e2e.spec.ts +++ b/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.e2e.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.describe('Tool - Phone parser and formatter', () => { test.beforeEach(async ({ page }) => { diff --git a/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.models.ts b/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.models.ts index e8bbb17..0a0fb63 100644 --- a/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.models.ts +++ b/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.models.ts @@ -18,13 +18,17 @@ const typeToLabel: Record<NonNullable<NumberType>, string> = { }; function formatTypeToHumanReadable(type: NumberType): string | undefined { - if (!type) return undefined; + if (!type) { + return undefined; + } return typeToLabel[type]; } function getFullCountryName(countryCode: string | undefined) { - if (!countryCode) return undefined; + if (!countryCode) { + return undefined; + } return lookup.byIso(countryCode)?.country; } @@ -35,7 +39,9 @@ function getDefaultCountryCode({ }: { locale?: string; defaultCode?: CountryCode } = {}): CountryCode { const countryCode = locale.split('-')[1]?.toUpperCase(); - if (!countryCode) return defaultCode; + if (!countryCode) { + return defaultCode; + } return (lookup.byIso(countryCode)?.iso2 ?? defaultCode) as CountryCode; } diff --git a/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue b/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue index 1f31085..647c319 100644 --- a/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue +++ b/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue @@ -1,44 +1,14 @@ -<template> - <div> - <n-form-item label="Default country code:"> - <n-select v-model:value="defaultCountryCode" :options="countriesOptions" /> - </n-form-item> - - <c-input-text - v-model:value="rawPhone" - placeholder="Enter a phone number" - label="Phone number:" - :validation="validation" - mb-5 - /> - - <n-table v-if="parsedDetails"> - <tbody> - <tr v-for="{ label, value } in parsedDetails" :key="label"> - <td> - <n-text strong>{{ label }}</n-text> - </td> - <td> - <span-copyable v-if="value" :value="value"></span-copyable> - <n-text v-else depth="3" italic>Unknown</n-text> - </td> - </tr> - </tbody> - </n-table> - </div> -</template> - <script setup lang="ts"> -import { withDefaultOnError } from '@/utils/defaults'; -import { parsePhoneNumber, getCountries, getCountryCallingCode } from 'libphonenumber-js/max'; -import { booleanToHumanReadable } from '@/utils/boolean'; -import { useValidation } from '@/composable/validation'; +import { getCountries, getCountryCallingCode, parsePhoneNumber } from 'libphonenumber-js/max'; import lookup from 'country-code-lookup'; import { formatTypeToHumanReadable, - getFullCountryName, getDefaultCountryCode, + getFullCountryName, } from './phone-parser-and-formatter.models'; +import { withDefaultOnError } from '@/utils/defaults'; +import { booleanToHumanReadable } from '@/utils/boolean'; +import { useValidation } from '@/composable/validation'; const rawPhone = ref(''); const defaultCountryCode = ref(getDefaultCountryCode()); @@ -46,18 +16,22 @@ const validation = useValidation({ source: rawPhone, rules: [ { - validator: (value) => value === '' || /^[0-9 +\-()]+$/.test(value), + validator: value => value === '' || /^[0-9 +\-()]+$/.test(value), message: 'Invalid phone number', }, ], }); const parsedDetails = computed(() => { - if (!validation.isValid) return undefined; + if (!validation.isValid) { + return undefined; + } const parsed = withDefaultOnError(() => parsePhoneNumber(rawPhone.value, defaultCountryCode.value), undefined); - if (!parsed) return undefined; + if (!parsed) { + return undefined; + } return [ { @@ -103,10 +77,42 @@ const parsedDetails = computed(() => { ]; }); -const countriesOptions = getCountries().map((code) => ({ +const countriesOptions = getCountries().map(code => ({ label: `${lookup.byIso(code)?.country || code} (+${getCountryCallingCode(code)})`, value: code, })); </script> -<style lang="less" scoped></style> +<template> + <div> + <n-form-item label="Default country code:"> + <n-select v-model:value="defaultCountryCode" :options="countriesOptions" /> + </n-form-item> + + <c-input-text + v-model:value="rawPhone" + placeholder="Enter a phone number" + label="Phone number:" + :validation="validation" + mb-5 + /> + + <n-table v-if="parsedDetails"> + <tbody> + <tr v-for="{ label, value } in parsedDetails" :key="label"> + <td> + <n-text strong> + {{ label }} + </n-text> + </td> + <td> + <span-copyable v-if="value" :value="value" /> + <n-text v-else depth="3" italic> + Unknown + </n-text> + </td> + </tr> + </tbody> + </n-table> + </div> +</template> diff --git a/src/tools/qr-code-generator/qr-code-generator.vue b/src/tools/qr-code-generator/qr-code-generator.vue index 12fcf11..f6ea7c8 100644 --- a/src/tools/qr-code-generator/qr-code-generator.vue +++ b/src/tools/qr-code-generator/qr-code-generator.vue @@ -1,3 +1,29 @@ +<script setup lang="ts"> +import { ref } from 'vue'; +import type { QRCodeErrorCorrectionLevel } from 'qrcode'; +import { useQRCode } from './useQRCode'; +import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; + +const foreground = ref('#000000ff'); +const background = ref('#ffffffff'); +const errorCorrectionLevel = ref<QRCodeErrorCorrectionLevel>('medium'); + +const errorCorrectionLevels = ['low', 'medium', 'quartile', 'high']; + +const text = ref('https://it-tools.tech'); +const { qrcode } = useQRCode({ + text, + color: { + background, + foreground, + }, + errorCorrectionLevel, + options: { width: 1024 }, +}); + +const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-code.png' }); +</script> + <template> <c-card> <n-grid x-gap="12" y-gap="12" cols="1 600:3"> @@ -28,35 +54,11 @@ <n-gi> <div flex flex-col items-center gap-3> <n-image :src="qrcode" width="200" /> - <c-button @click="download"> Download qr-code </c-button> + <c-button @click="download"> + Download qr-code + </c-button> </div> </n-gi> </n-grid> </c-card> </template> - -<script setup lang="ts"> -import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; -import { ref } from 'vue'; -import type { QRCodeErrorCorrectionLevel } from 'qrcode'; -import { useQRCode } from './useQRCode'; - -const foreground = ref('#000000ff'); -const background = ref('#ffffffff'); -const errorCorrectionLevel = ref<QRCodeErrorCorrectionLevel>('medium'); - -const errorCorrectionLevels = ['low', 'medium', 'quartile', 'high']; - -const text = ref('https://it-tools.tech'); -const { qrcode } = useQRCode({ - text, - color: { - background, - foreground, - }, - errorCorrectionLevel, - options: { width: 1024 }, -}); - -const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-code.png' }); -</script> diff --git a/src/tools/qr-code-generator/useQRCode.ts b/src/tools/qr-code-generator/useQRCode.ts index 64ee90a..5aa5450 100644 --- a/src/tools/qr-code-generator/useQRCode.ts +++ b/src/tools/qr-code-generator/useQRCode.ts @@ -1,6 +1,6 @@ -import { get, type MaybeRef } from '@vueuse/core'; +import { type MaybeRef, get } from '@vueuse/core'; import QRCode, { type QRCodeErrorCorrectionLevel, type QRCodeToDataURLOptions } from 'qrcode'; -import { ref, watch, isRef } from 'vue'; +import { isRef, ref, watch } from 'vue'; export function useQRCode({ text, @@ -8,17 +8,17 @@ export function useQRCode({ errorCorrectionLevel, options, }: { - text: MaybeRef<string>; - color: { foreground: MaybeRef<string>; background: MaybeRef<string> }; - errorCorrectionLevel?: MaybeRef<QRCodeErrorCorrectionLevel>; - options?: QRCodeToDataURLOptions; + text: MaybeRef<string> + color: { foreground: MaybeRef<string>; background: MaybeRef<string> } + errorCorrectionLevel?: MaybeRef<QRCodeErrorCorrectionLevel> + options?: QRCodeToDataURLOptions }) { const qrcode = ref(''); watch( [text, background, foreground, errorCorrectionLevel].filter(isRef), async () => { - if (get(text)) + if (get(text)) { qrcode.value = await QRCode.toDataURL(get(text).trim(), { color: { dark: get(foreground), @@ -28,6 +28,7 @@ export function useQRCode({ errorCorrectionLevel: get(errorCorrectionLevel) ?? 'M', ...options, }); + } }, { immediate: true }, ); diff --git a/src/tools/random-port-generator/random-port-generator.vue b/src/tools/random-port-generator/random-port-generator.vue index 8e3ecb6..e4ac609 100644 --- a/src/tools/random-port-generator/random-port-generator.vue +++ b/src/tools/random-port-generator/random-port-generator.vue @@ -1,25 +1,29 @@ +<script setup lang="ts"> +import { generatePort } from './random-port-generator.model'; +import { computedRefreshable } from '@/composable/computedRefreshable'; +import { useCopy } from '@/composable/copy'; + +const [port, refreshPort] = computedRefreshable(() => String(generatePort())); + +const { copy } = useCopy({ source: port, text: 'Port copied to the clipboard' }); +</script> + <template> <c-card> <div class="port"> {{ port }} </div> <div flex justify-center gap-3> - <c-button @click="copy"> Copy </c-button> - <c-button @click="refreshPort"> Refresh </c-button> + <c-button @click="copy"> + Copy + </c-button> + <c-button @click="refreshPort"> + Refresh + </c-button> </div> </c-card> </template> -<script setup lang="ts"> -import { computedRefreshable } from '@/composable/computedRefreshable'; -import { useCopy } from '@/composable/copy'; -import { generatePort } from './random-port-generator.model'; - -const [port, refreshPort] = computedRefreshable(() => String(generatePort())); - -const { copy } = useCopy({ source: port, text: 'Port copied to the clipboard' }); -</script> - <style lang="less" scoped> .port { text-align: center; diff --git a/src/tools/roman-numeral-converter/roman-numeral-converter.service.test.ts b/src/tools/roman-numeral-converter/roman-numeral-converter.service.test.ts index 5ec9dd4..86dccc9 100644 --- a/src/tools/roman-numeral-converter/roman-numeral-converter.service.test.ts +++ b/src/tools/roman-numeral-converter/roman-numeral-converter.service.test.ts @@ -1,4 +1,4 @@ -import { expect, describe, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { arabicToRoman } from './roman-numeral-converter.service'; describe('roman-numeral-converter', () => { diff --git a/src/tools/roman-numeral-converter/roman-numeral-converter.service.ts b/src/tools/roman-numeral-converter/roman-numeral-converter.service.ts index 98afec6..3049391 100644 --- a/src/tools/roman-numeral-converter/roman-numeral-converter.service.ts +++ b/src/tools/roman-numeral-converter/roman-numeral-converter.service.ts @@ -1,7 +1,9 @@ export const MIN_ARABIC_TO_ROMAN = 1; export const MAX_ARABIC_TO_ROMAN = 3999; export function arabicToRoman(num: number) { - if (num < MIN_ARABIC_TO_ROMAN || num > MAX_ARABIC_TO_ROMAN) return ''; + if (num < MIN_ARABIC_TO_ROMAN || num > MAX_ARABIC_TO_ROMAN) { + return ''; + } const lookup: { [key: string]: number } = { M: 1000, @@ -28,7 +30,7 @@ export function arabicToRoman(num: number) { return roman; } -const ROMAN_NUMBER_REGEX = new RegExp(/^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/); +const ROMAN_NUMBER_REGEX = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/; export function isValidRomanNumber(romanNumber: string) { return ROMAN_NUMBER_REGEX.test(romanNumber); diff --git a/src/tools/roman-numeral-converter/roman-numeral-converter.vue b/src/tools/roman-numeral-converter/roman-numeral-converter.vue index 1146865..e957b00 100644 --- a/src/tools/roman-numeral-converter/roman-numeral-converter.vue +++ b/src/tools/roman-numeral-converter/roman-numeral-converter.vue @@ -1,42 +1,14 @@ -<template> - <div> - <c-card title="Arabic to roman"> - <div flex items-center justify-between> - <n-form-item v-bind="validationNumeral as any"> - <n-input-number v-model:value="inputNumeral" :min="1" style="width: 200px" :show-button="false" /> - </n-form-item> - <div class="result"> - {{ outputRoman }} - </div> - <c-button autofocus :disabled="validationNumeral.validationStatus === 'error'" @click="copyRoman"> - Copy - </c-button> - </div> - </c-card> - <c-card title="Roman to arabic" mt-5> - <div flex items-center justify-between> - <c-input-text v-model:value="inputRoman" style="width: 200px" :validation="validationRoman" /> - - <div class="result"> - {{ outputNumeral }} - </div> - <c-button :disabled="!validationRoman.isValid" @click="copyArabic"> Copy </c-button> - </div> - </c-card> - </div> -</template> - <script setup lang="ts"> -import { useCopy } from '@/composable/copy'; -import { ref, computed } from 'vue'; -import { useValidation } from '@/composable/validation'; +import { computed, ref } from 'vue'; import { - arabicToRoman, - romanToArabic, MAX_ARABIC_TO_ROMAN, MIN_ARABIC_TO_ROMAN, + arabicToRoman, isValidRomanNumber, + romanToArabic, } from './roman-numeral-converter.service'; +import { useCopy } from '@/composable/copy'; +import { useValidation } from '@/composable/validation'; const inputNumeral = ref(42); const outputRoman = computed(() => arabicToRoman(inputNumeral.value)); @@ -45,7 +17,7 @@ const { attrs: validationNumeral } = useValidation({ source: inputNumeral, rules: [ { - validator: (value) => value >= MIN_ARABIC_TO_ROMAN && value <= MAX_ARABIC_TO_ROMAN, + validator: value => value >= MIN_ARABIC_TO_ROMAN && value <= MAX_ARABIC_TO_ROMAN, message: `We can only convert numbers between ${MIN_ARABIC_TO_ROMAN.toLocaleString()} and ${MAX_ARABIC_TO_ROMAN.toLocaleString()}`, }, ], @@ -58,8 +30,8 @@ const validationRoman = useValidation({ source: inputRoman, rules: [ { - validator: (value) => isValidRomanNumber(value), - message: `The input you entered is not a valid roman number`, + validator: value => isValidRomanNumber(value), + message: 'The input you entered is not a valid roman number', }, ], }); @@ -68,6 +40,36 @@ const { copy: copyRoman } = useCopy({ source: outputRoman, text: 'Roman number c const { copy: copyArabic } = useCopy({ source: outputNumeral, text: 'Arabic number copied to the clipboard' }); </script> +<template> + <div> + <c-card title="Arabic to roman"> + <div flex items-center justify-between> + <n-form-item v-bind="validationNumeral as any"> + <n-input-number v-model:value="inputNumeral" :min="1" style="width: 200px" :show-button="false" /> + </n-form-item> + <div class="result"> + {{ outputRoman }} + </div> + <c-button autofocus :disabled="validationNumeral.validationStatus === 'error'" @click="copyRoman"> + Copy + </c-button> + </div> + </c-card> + <c-card title="Roman to arabic" mt-5> + <div flex items-center justify-between> + <c-input-text v-model:value="inputRoman" style="width: 200px" :validation="validationRoman" /> + + <div class="result"> + {{ outputNumeral }} + </div> + <c-button :disabled="!validationRoman.isValid" @click="copyArabic"> + Copy + </c-button> + </div> + </c-card> + </div> +</template> + <style lang="less" scoped> .result { font-size: 22px; diff --git a/src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue b/src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue index 80f71b2..021d7e8 100644 --- a/src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue +++ b/src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue @@ -1,32 +1,10 @@ -<template> - <div style="flex: 0 0 100%"> - <div item-style="flex: 1 1 0" style="max-width: 600px" mx-auto flex gap-3> - <n-form-item label="Bits :" v-bind="bitsValidationAttrs as any" label-placement="left" label-width="100"> - <n-input-number v-model:value="bits" min="256" max="16384" step="8" /> - </n-form-item> - - <c-button @click="refreshCerts">Refresh key-pair</c-button> - </div> - </div> - - <div> - <h3>Public key</h3> - <textarea-copyable :value="certs.publicKeyPem" /> - </div> - - <div> - <h3>Private key</h3> - <textarea-copyable :value="certs.privateKeyPem" /> - </div> -</template> - <script setup lang="ts"> -import TextareaCopyable from '@/components/TextareaCopyable.vue'; import { ref } from 'vue'; +import { generateKeyPair } from './rsa-key-pair-generator.service'; +import TextareaCopyable from '@/components/TextareaCopyable.vue'; import { withDefaultOnErrorAsync } from '@/utils/defaults'; import { useValidation } from '@/composable/validation'; import { computedRefreshableAsync } from '@/composable/computedRefreshable'; -import { generateKeyPair } from './rsa-key-pair-generator.service'; const bits = ref(2048); const emptyCerts = { publicKeyPem: '', privateKeyPem: '' }; @@ -36,7 +14,7 @@ const { attrs: bitsValidationAttrs } = useValidation({ rules: [ { message: 'Bits should be 256 <= bits <= 16384 and be a multiple of 8', - validator: (value) => value >= 256 && value <= 16384 && value % 8 === 0, + validator: value => value >= 256 && value <= 16384 && value % 8 === 0, }, ], }); @@ -47,4 +25,26 @@ const [certs, refreshCerts] = computedRefreshableAsync( ); </script> -<style lang="less" scoped></style> +<template> + <div style="flex: 0 0 100%"> + <div item-style="flex: 1 1 0" style="max-width: 600px" mx-auto flex gap-3> + <n-form-item label="Bits :" v-bind="bitsValidationAttrs as any" label-placement="left" label-width="100"> + <n-input-number v-model:value="bits" min="256" max="16384" step="8" /> + </n-form-item> + + <c-button @click="refreshCerts"> + Refresh key-pair + </c-button> + </div> + </div> + + <div> + <h3>Public key</h3> + <TextareaCopyable :value="certs.publicKeyPem" /> + </div> + + <div> + <h3>Private key</h3> + <TextareaCopyable :value="certs.privateKeyPem" /> + </div> +</template> diff --git a/src/tools/slugify-string/slugify-string.vue b/src/tools/slugify-string/slugify-string.vue index c140e21..0f5ddc2 100644 --- a/src/tools/slugify-string/slugify-string.vue +++ b/src/tools/slugify-string/slugify-string.vue @@ -1,7 +1,18 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import slugify from '@sindresorhus/slugify'; +import { withDefaultOnError } from '@/utils/defaults'; +import { useCopy } from '@/composable/copy'; + +const input = ref(''); +const slug = computed(() => withDefaultOnError(() => slugify(input.value), '')); +const { copy } = useCopy({ source: slug, text: 'Slug copied to clipboard' }); +</script> + <template> <div> <n-form-item label="Your string to slugify"> - <n-input v-model:value="input" type="textarea" placeholder="Put your string here (ex: My file path)"></n-input> + <n-input v-model:value="input" type="textarea" placeholder="Put your string here (ex: My file path)" /> </n-form-item> <n-form-item label="Your slug"> @@ -10,24 +21,13 @@ type="textarea" readonly placeholder="You slug will be generated here (ex: my-file-path)" - ></n-input> + /> </n-form-item> <div flex justify-center> - <c-button :disabled="slug.length === 0" @click="copy">Copy slug</c-button> + <c-button :disabled="slug.length === 0" @click="copy"> + Copy slug + </c-button> </div> </div> </template> - -<script setup lang="ts"> -import { computed, ref } from 'vue'; -import slugify from '@sindresorhus/slugify'; -import { withDefaultOnError } from '@/utils/defaults'; -import { useCopy } from '@/composable/copy'; - -const input = ref(''); -const slug = computed(() => withDefaultOnError(() => slugify(input.value), '')); -const { copy } = useCopy({ source: slug, text: 'Slug copied to clipboard' }); -</script> - -<style lang="less" scoped></style> diff --git a/src/tools/sql-prettify/sql-prettify.vue b/src/tools/sql-prettify/sql-prettify.vue index b8d4aa4..2ee93da 100644 --- a/src/tools/sql-prettify/sql-prettify.vue +++ b/src/tools/sql-prettify/sql-prettify.vue @@ -1,3 +1,23 @@ +<script setup lang="ts"> +import { type FormatFnOptions, format as formatSQL } from 'sql-formatter'; +import { computed, reactive, ref } from 'vue'; +import TextareaCopyable from '@/components/TextareaCopyable.vue'; +import { useStyleStore } from '@/stores/style.store'; + +const inputElement = ref<HTMLElement>(); +const styleStore = useStyleStore(); +const config = reactive<Partial<FormatFnOptions>>({ + keywordCase: 'upper', + useTabs: false, + language: 'sql', + indentStyle: 'standard', + tabulateAlias: true, +}); + +const rawSQL = ref('select field1,field2,field3 from my_table where my_condition;'); +const prettySQL = computed(() => formatSQL(rawSQL.value, config)); +</script> + <template> <div style="flex: 0 0 100%"> <div mx-auto style="max-width: 600px" flex gap-2 :class="{ 'flex-col': styleStore.isSmallScreen }"> @@ -58,30 +78,10 @@ /> </n-form-item> <n-form-item label="Prettify version of your query"> - <textarea-copyable :value="prettySQL" language="sql" :follow-height-of="inputElement" /> + <TextareaCopyable :value="prettySQL" language="sql" :follow-height-of="inputElement" /> </n-form-item> </template> -<script setup lang="ts"> -import TextareaCopyable from '@/components/TextareaCopyable.vue'; -import { useStyleStore } from '@/stores/style.store'; -import { format as formatSQL, type FormatFnOptions } from 'sql-formatter'; -import { computed, reactive, ref } from 'vue'; - -const inputElement = ref<HTMLElement>(); -const styleStore = useStyleStore(); -const config = reactive<Partial<FormatFnOptions>>({ - keywordCase: 'upper', - useTabs: false, - language: 'sql', - indentStyle: 'standard', - tabulateAlias: true, -}); - -const rawSQL = ref('select field1,field2,field3 from my_table where my_condition;'); -const prettySQL = computed(() => formatSQL(rawSQL.value, config)); -</script> - <style lang="less" scoped> .result-card { position: relative; diff --git a/src/tools/svg-placeholder-generator/svg-placeholder-generator.vue b/src/tools/svg-placeholder-generator/svg-placeholder-generator.vue index 0ff61a6..0c454ee 100644 --- a/src/tools/svg-placeholder-generator/svg-placeholder-generator.vue +++ b/src/tools/svg-placeholder-generator/svg-placeholder-generator.vue @@ -1,3 +1,37 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import TextareaCopyable from '@/components/TextareaCopyable.vue'; +import { useCopy } from '@/composable/copy'; +import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; +import { textToBase64 } from '@/utils/base64'; + +const width = ref(600); +const height = ref(350); +const fontSize = ref(26); +const bgColor = ref('#cccccc'); +const fgColor = ref('#333333'); +const useExactSize = ref(true); +const customText = ref(''); +const svgString = computed(() => { + const w = width.value; + const h = height.value; + const text = customText.value.length > 0 ? customText.value : `${w}x${h}`; + const size = useExactSize.value ? ` width="${w}" height="${h}"` : ''; + + return ` +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}"${size}> + <rect width="${w}" height="${h}" fill="${bgColor.value}"></rect> + <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="monospace" font-size="${fontSize.value}px" fill="${fgColor.value}">${text}</text> +</svg> + `.trim(); +}); +const base64 = computed(() => `data:image/svg+xml;base64,${textToBase64(svgString.value)}`); + +const { copy: copySVG } = useCopy({ source: svgString }); +const { copy: copyBase64 } = useCopy({ source: base64 }); +const { download } = useDownloadFileFromBase64({ source: base64 }); +</script> + <template> <div> <n-form label-placement="left" label-width="100"> @@ -38,56 +72,28 @@ </n-form> <n-form-item label="SVG HTML element"> - <textarea-copyable :value="svgString" copy-placement="none" /> + <TextareaCopyable :value="svgString" copy-placement="none" /> </n-form-item> <n-form-item label="SVG in Base64"> - <textarea-copyable :value="base64" copy-placement="none" /> + <TextareaCopyable :value="base64" copy-placement="none" /> </n-form-item> <div flex justify-center gap-3> - <c-button @click="copySVG()">Copy svg</c-button> - <c-button @click="copyBase64()">Copy base64</c-button> - <c-button @click="download()">Download svg</c-button> + <c-button @click="copySVG()"> + Copy svg + </c-button> + <c-button @click="copyBase64()"> + Copy base64 + </c-button> + <c-button @click="download()"> + Download svg + </c-button> </div> </div> - <img :src="base64" alt="Image" /> + <img :src="base64" alt="Image"> </template> -<script setup lang="ts"> -import TextareaCopyable from '@/components/TextareaCopyable.vue'; -import { useCopy } from '@/composable/copy'; -import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; -import { textToBase64 } from '@/utils/base64'; -import { computed, ref } from 'vue'; - -const width = ref(600); -const height = ref(350); -const fontSize = ref(26); -const bgColor = ref('#cccccc'); -const fgColor = ref('#333333'); -const useExactSize = ref(true); -const customText = ref(''); -const svgString = computed(() => { - const w = width.value; - const h = height.value; - const text = customText.value.length > 0 ? customText.value : `${w}x${h}`; - const size = useExactSize.value ? ` width="${w}" height="${h}"` : ''; - - return ` -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}"${size}> - <rect width="${w}" height="${h}" fill="${bgColor.value}"></rect> - <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="monospace" font-size="${fontSize.value}px" fill="${fgColor.value}">${text}</text> -</svg> - `.trim(); -}); -const base64 = computed(() => 'data:image/svg+xml;base64,' + textToBase64(svgString.value)); - -const { copy: copySVG } = useCopy({ source: svgString }); -const { copy: copyBase64 } = useCopy({ source: base64 }); -const { download } = useDownloadFileFromBase64({ source: base64 }); -</script> - <style lang="less" scoped> .n-input-number { width: 100%; diff --git a/src/tools/temperature-converter/temperature-converter.vue b/src/tools/temperature-converter/temperature-converter.vue index 2c33627..c563246 100644 --- a/src/tools/temperature-converter/temperature-converter.vue +++ b/src/tools/temperature-converter/temperature-converter.vue @@ -1,23 +1,3 @@ -<template> - <div> - <n-input-group v-for="[key, { title, unit }] in Object.entries(units)" :key="key" mb-3 w-full> - <n-input-group-label style="width: 100px"> - {{ title }} - </n-input-group-label> - - <n-input-number - v-model:value="units[key].ref" - style="flex: 1" - @update:value="() => update(key as TemperatureScale)" - /> - - <n-input-group-label style="width: 50px"> - {{ unit }} - </n-input-group-label> - </n-input-group> - </div> -</template> - <script setup lang="ts"> import _ from 'lodash'; import { reactive } from 'vue'; @@ -45,64 +25,64 @@ const units = reactive< string | TemperatureScale, { title: string; unit: string; ref: number; toKelvin: (v: number) => number; fromKelvin: (v: number) => number } > ->({ - kelvin: { - title: 'Kelvin', - unit: 'K', - ref: 0, - toKelvin: _.identity, - fromKelvin: _.identity, - }, - celsius: { - title: 'Celsius', - unit: '°C', - ref: 0, - toKelvin: convertCelsiusToKelvin, - fromKelvin: convertKelvinToCelsius, - }, - fahrenheit: { - title: 'Fahrenheit', - unit: '°F', - ref: 0, - toKelvin: convertFahrenheitToKelvin, - fromKelvin: convertKelvinToFahrenheit, - }, - rankine: { - title: 'Rankine', - unit: '°R', - ref: 0, - toKelvin: convertRankineToKelvin, - fromKelvin: convertKelvinToRankine, - }, - delisle: { - title: 'Delisle', - unit: '°De', - ref: 0, - toKelvin: convertDelisleToKelvin, - fromKelvin: convertKelvinToDelisle, - }, - newton: { - title: 'Newton', - unit: '°N', - ref: 0, - toKelvin: convertNewtonToKelvin, - fromKelvin: convertKelvinToNewton, - }, - reaumur: { - title: 'Réaumur', - unit: '°Ré', - ref: 0, - toKelvin: convertReaumurToKelvin, - fromKelvin: convertKelvinToReaumur, - }, - romer: { - title: 'Rømer', - unit: '°Rø', - ref: 0, - toKelvin: convertRomerToKelvin, - fromKelvin: convertKelvinToRomer, - }, -}); + >({ + kelvin: { + title: 'Kelvin', + unit: 'K', + ref: 0, + toKelvin: _.identity, + fromKelvin: _.identity, + }, + celsius: { + title: 'Celsius', + unit: '°C', + ref: 0, + toKelvin: convertCelsiusToKelvin, + fromKelvin: convertKelvinToCelsius, + }, + fahrenheit: { + title: 'Fahrenheit', + unit: '°F', + ref: 0, + toKelvin: convertFahrenheitToKelvin, + fromKelvin: convertKelvinToFahrenheit, + }, + rankine: { + title: 'Rankine', + unit: '°R', + ref: 0, + toKelvin: convertRankineToKelvin, + fromKelvin: convertKelvinToRankine, + }, + delisle: { + title: 'Delisle', + unit: '°De', + ref: 0, + toKelvin: convertDelisleToKelvin, + fromKelvin: convertKelvinToDelisle, + }, + newton: { + title: 'Newton', + unit: '°N', + ref: 0, + toKelvin: convertNewtonToKelvin, + fromKelvin: convertKelvinToNewton, + }, + reaumur: { + title: 'Réaumur', + unit: '°Ré', + ref: 0, + toKelvin: convertReaumurToKelvin, + fromKelvin: convertKelvinToReaumur, + }, + romer: { + title: 'Rømer', + unit: '°Rø', + ref: 0, + toKelvin: convertRomerToKelvin, + fromKelvin: convertKelvinToRomer, + }, + }); function update(key: TemperatureScale) { const { ref: value, toKelvin } = units[key]; @@ -120,4 +100,22 @@ function update(key: TemperatureScale) { update('kelvin'); </script> -<style lang="less" scoped></style> +<template> + <div> + <n-input-group v-for="[key, { title, unit }] in Object.entries(units)" :key="key" mb-3 w-full> + <n-input-group-label style="width: 100px"> + {{ title }} + </n-input-group-label> + + <n-input-number + v-model:value="units[key].ref" + style="flex: 1" + @update:value="() => update(key as TemperatureScale)" + /> + + <n-input-group-label style="width: 50px"> + {{ unit }} + </n-input-group-label> + </n-input-group> + </div> +</template> diff --git a/src/tools/text-statistics/index.ts b/src/tools/text-statistics/index.ts index def0b6d..0e54b71 100644 --- a/src/tools/text-statistics/index.ts +++ b/src/tools/text-statistics/index.ts @@ -4,7 +4,7 @@ import { defineTool } from '../tool'; export const tool = defineTool({ name: 'Text statistics', path: '/text-statistics', - description: "Get information about a text, the amount of characters, the amount of words, it's size, ...", + description: 'Get information about a text, the amount of characters, the amount of words, it\'s size, ...', keywords: ['text', 'statistics', 'length', 'characters', 'count', 'size', 'bytes'], component: () => import('./text-statistics.vue'), icon: FileText, diff --git a/src/tools/text-statistics/text-statistics.service.test.ts b/src/tools/text-statistics/text-statistics.service.test.ts index 5260683..18ffc39 100644 --- a/src/tools/text-statistics/text-statistics.service.test.ts +++ b/src/tools/text-statistics/text-statistics.service.test.ts @@ -1,4 +1,4 @@ -import { expect, describe, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { getStringSizeInBytes } from './text-statistics.service'; describe('text-statistics', () => { diff --git a/src/tools/text-statistics/text-statistics.vue b/src/tools/text-statistics/text-statistics.vue index a03915c..ec543fa 100644 --- a/src/tools/text-statistics/text-statistics.vue +++ b/src/tools/text-statistics/text-statistics.vue @@ -1,3 +1,11 @@ +<script setup lang="ts"> +import { ref } from 'vue'; +import { getStringSizeInBytes } from './text-statistics.service'; +import { formatBytes } from '@/utils/convert'; + +const text = ref(''); +</script> + <template> <c-card> <n-input v-model:value="text" type="textarea" placeholder="Your text..." rows="5" /> @@ -10,11 +18,3 @@ </div> </c-card> </template> - -<script setup lang="ts"> -import { formatBytes } from '@/utils/convert'; -import { ref } from 'vue'; -import { getStringSizeInBytes } from './text-statistics.service'; - -const text = ref(''); -</script> diff --git a/src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue b/src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue index 9ded4ff..75b2334 100644 --- a/src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue +++ b/src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue @@ -1,3 +1,13 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import { textToNatoAlphabet } from './text-to-nato-alphabet.service'; +import { useCopy } from '@/composable/copy'; + +const input = ref(''); +const natoText = computed(() => textToNatoAlphabet({ text: input.value })); +const { copy } = useCopy({ source: natoText, text: 'NATO alphabet string copied.' }); +</script> + <template> <div> <c-input-text @@ -9,26 +19,18 @@ /> <div v-if="natoText"> - <n-text mb-1 block>Your text in NATO phonetic alphabet</n-text> + <n-text mb-1 block> + Your text in NATO phonetic alphabet + </n-text> <c-card> {{ natoText }} </c-card> <div mt-3 flex justify-center> - <c-button autofocus @click="copy"> Copy NATO string </c-button> + <c-button autofocus @click="copy"> + Copy NATO string + </c-button> </div> </div> </div> </template> - -<script setup lang="ts"> -import { useCopy } from '@/composable/copy'; -import { computed, ref } from 'vue'; -import { textToNatoAlphabet } from './text-to-nato-alphabet.service'; - -const input = ref(''); -const natoText = computed(() => textToNatoAlphabet({ text: input.value })); -const { copy } = useCopy({ source: natoText, text: 'NATO alphabet string copied.' }); -</script> - -<style lang="less" scoped></style> diff --git a/src/tools/token-generator/token-generator.e2e.spec.ts b/src/tools/token-generator/token-generator.e2e.spec.ts index 905a81c..444c538 100644 --- a/src/tools/token-generator/token-generator.e2e.spec.ts +++ b/src/tools/token-generator/token-generator.e2e.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.describe('Tool - Token generator', () => { test.beforeEach(async ({ page }) => { diff --git a/src/tools/token-generator/token-generator.service.test.ts b/src/tools/token-generator/token-generator.service.test.ts index ed9dab3..604f5a8 100644 --- a/src/tools/token-generator/token-generator.service.test.ts +++ b/src/tools/token-generator/token-generator.service.test.ts @@ -1,4 +1,4 @@ -import { expect, describe, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { createToken } from './token-generator.service'; describe('token-generator', () => { diff --git a/src/tools/token-generator/token-generator.service.ts b/src/tools/token-generator/token-generator.service.ts index f48a4de..1885d24 100644 --- a/src/tools/token-generator/token-generator.service.ts +++ b/src/tools/token-generator/token-generator.service.ts @@ -8,16 +8,16 @@ export function createToken({ length = 64, alphabet, }: { - withUppercase?: boolean; - withLowercase?: boolean; - withNumbers?: boolean; - withSymbols?: boolean; - length?: number; - alphabet?: string; + withUppercase?: boolean + withLowercase?: boolean + withNumbers?: boolean + withSymbols?: boolean + length?: number + alphabet?: string }) { - const allAlphabet = - alphabet ?? - [ + const allAlphabet + = alphabet + ?? [ ...(withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : ''), ...(withLowercase ? 'abcdefghijklmopqrstuvwxyz' : ''), ...(withNumbers ? '0123456789' : ''), diff --git a/src/tools/token-generator/token-generator.tool.vue b/src/tools/token-generator/token-generator.tool.vue index bdfb4da..81b7b58 100644 --- a/src/tools/token-generator/token-generator.tool.vue +++ b/src/tools/token-generator/token-generator.tool.vue @@ -1,3 +1,28 @@ +<script setup lang="ts"> +import { createToken } from './token-generator.service'; +import { useCopy } from '@/composable/copy'; +import { useQueryParam } from '@/composable/queryParams'; +import { computedRefreshable } from '@/composable/computedRefreshable'; + +const length = useQueryParam({ name: 'length', defaultValue: 64 }); +const withUppercase = useQueryParam({ name: 'uppercase', defaultValue: true }); +const withLowercase = useQueryParam({ name: 'lowercase', defaultValue: true }); +const withNumbers = useQueryParam({ name: 'numbers', defaultValue: true }); +const withSymbols = useQueryParam({ name: 'symbols', defaultValue: false }); + +const [token, refreshToken] = computedRefreshable(() => + createToken({ + length: length.value, + withUppercase: withUppercase.value, + withLowercase: withLowercase.value, + withNumbers: withNumbers.value, + withSymbols: withSymbols.value, + }), +); + +const { copy } = useCopy({ source: token, text: 'Token copied to the clipboard' }); +</script> + <template> <div> <c-card> @@ -43,34 +68,13 @@ /> <div mt-5 flex justify-center gap-3> - <c-button @click="copy"> Copy </c-button> - <c-button @click="refreshToken"> Refresh </c-button> + <c-button @click="copy"> + Copy + </c-button> + <c-button @click="refreshToken"> + Refresh + </c-button> </div> </c-card> </div> </template> - -<script setup lang="ts"> -import { useCopy } from '@/composable/copy'; -import { useQueryParam } from '@/composable/queryParams'; -import { computedRefreshable } from '@/composable/computedRefreshable'; -import { createToken } from './token-generator.service'; - -const length = useQueryParam({ name: 'length', defaultValue: 64 }); -const withUppercase = useQueryParam({ name: 'uppercase', defaultValue: true }); -const withLowercase = useQueryParam({ name: 'lowercase', defaultValue: true }); -const withNumbers = useQueryParam({ name: 'numbers', defaultValue: true }); -const withSymbols = useQueryParam({ name: 'symbols', defaultValue: false }); - -const [token, refreshToken] = computedRefreshable(() => - createToken({ - length: length.value, - withUppercase: withUppercase.value, - withLowercase: withLowercase.value, - withNumbers: withNumbers.value, - withSymbols: withSymbols.value, - }), -); - -const { copy } = useCopy({ source: token, text: 'Token copied to the clipboard' }); -</script> diff --git a/src/tools/tools.store.ts b/src/tools/tools.store.ts index 2b0826c..769b4d8 100644 --- a/src/tools/tools.store.ts +++ b/src/tools/tools.store.ts @@ -1,8 +1,8 @@ -import { get, useStorage, type MaybeRef } from '@vueuse/core'; +import { type MaybeRef, get, useStorage } from '@vueuse/core'; import { defineStore } from 'pinia'; import type { Ref } from 'vue'; -import { toolsWithCategory } from './index'; import type { Tool, ToolWithCategory } from './tools.types'; +import { toolsWithCategory } from './index'; export const useToolStore = defineStore('tools', { state: () => ({ @@ -11,12 +11,12 @@ export const useToolStore = defineStore('tools', { getters: { favoriteTools(state) { return state.favoriteToolsName - .map((favoriteName) => toolsWithCategory.find(({ name }) => name === favoriteName)) + .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)); + return toolsWithCategory.filter(tool => !state.favoriteToolsName.includes(tool.name)); }, tools(): ToolWithCategory[] { @@ -34,7 +34,7 @@ export const useToolStore = defineStore('tools', { }, removeToolFromFavorites({ tool }: { tool: MaybeRef<Tool> }) { - this.favoriteToolsName = this.favoriteToolsName.filter((name) => get(tool).name !== name); + this.favoriteToolsName = this.favoriteToolsName.filter(name => get(tool).name !== name); }, isToolFavorite({ tool }: { tool: MaybeRef<Tool> }) { diff --git a/src/tools/tools.types.ts b/src/tools/tools.types.ts index 48f6062..dcef854 100644 --- a/src/tools/tools.types.ts +++ b/src/tools/tools.types.ts @@ -1,20 +1,20 @@ 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; - createdAt?: Date; -}; +export interface Tool { + name: string + path: string + description: string + keywords: string[] + component: () => Promise<Component> + icon: Component + redirectFrom?: string[] + isNew: boolean + createdAt?: Date +} -export type ToolCategory = { - name: string; - components: Tool[]; -}; +export interface ToolCategory { + name: string + components: Tool[] +} export type ToolWithCategory = Tool & { category: string }; diff --git a/src/tools/url-encoder/url-encoder.vue b/src/tools/url-encoder/url-encoder.vue index 4f89986..5fdcade 100644 --- a/src/tools/url-encoder/url-encoder.vue +++ b/src/tools/url-encoder/url-encoder.vue @@ -1,3 +1,41 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import { useCopy } from '@/composable/copy'; +import { useValidation } from '@/composable/validation'; +import { isNotThrowing } from '@/utils/boolean'; +import { withDefaultOnError } from '@/utils/defaults'; + +const encodeInput = ref('Hello world :)'); +const encodeOutput = computed(() => withDefaultOnError(() => encodeURIComponent(encodeInput.value), '')); + +const encodedValidation = useValidation({ + source: encodeInput, + rules: [ + { + validator: value => isNotThrowing(() => encodeURIComponent(value)), + message: 'Impossible to parse this string', + }, + ], +}); + +const { copy: copyEncoded } = useCopy({ source: encodeOutput, text: 'Encoded string copied to the clipboard' }); + +const decodeInput = ref('Hello%20world%20%3A)'); +const decodeOutput = computed(() => withDefaultOnError(() => decodeURIComponent(decodeInput.value), '')); + +const decodeValidation = useValidation({ + source: encodeInput, + rules: [ + { + validator: value => isNotThrowing(() => decodeURIComponent(value)), + message: 'Impossible to parse this string', + }, + ], +}); + +const { copy: copyDecoded } = useCopy({ source: decodeOutput, text: 'Decoded string copied to the clipboard' }); +</script> + <template> <c-card title="Encode"> <n-form-item @@ -24,7 +62,9 @@ </n-form-item> <div flex justify-center> - <c-button @click="copyEncoded"> Copy </c-button> + <c-button @click="copyEncoded"> + Copy + </c-button> </div> </c-card> <c-card title="Decode"> @@ -52,45 +92,9 @@ </n-form-item> <div flex justify-center> - <c-button @click="copyDecoded"> Copy </c-button> + <c-button @click="copyDecoded"> + Copy + </c-button> </div> </c-card> </template> - -<script setup lang="ts"> -import { useCopy } from '@/composable/copy'; -import { useValidation } from '@/composable/validation'; -import { isNotThrowing } from '@/utils/boolean'; -import { withDefaultOnError } from '@/utils/defaults'; -import { computed, ref } from 'vue'; - -const encodeInput = ref('Hello world :)'); -const encodeOutput = computed(() => withDefaultOnError(() => encodeURIComponent(encodeInput.value), '')); - -const encodedValidation = useValidation({ - source: encodeInput, - rules: [ - { - validator: (value) => isNotThrowing(() => encodeURIComponent(value)), - message: 'Impossible to parse this string', - }, - ], -}); - -const { copy: copyEncoded } = useCopy({ source: encodeOutput, text: 'Encoded string copied to the clipboard' }); - -const decodeInput = ref('Hello%20world%20%3A)'); -const decodeOutput = computed(() => withDefaultOnError(() => decodeURIComponent(decodeInput.value), '')); - -const decodeValidation = useValidation({ - source: encodeInput, - rules: [ - { - validator: (value) => isNotThrowing(() => decodeURIComponent(value)), - message: 'Impossible to parse this string', - }, - ], -}); - -const { copy: copyDecoded } = useCopy({ source: decodeOutput, text: 'Decoded string copied to the clipboard' }); -</script> diff --git a/src/tools/url-parser/url-parser.vue b/src/tools/url-parser/url-parser.vue index 04ab939..cadd07e 100644 --- a/src/tools/url-parser/url-parser.vue +++ b/src/tools/url-parser/url-parser.vue @@ -1,3 +1,30 @@ +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import InputCopyable from '../../components/InputCopyable.vue'; +import { isNotThrowing } from '@/utils/boolean'; +import { withDefaultOnError } from '@/utils/defaults'; + +const urlToParse = ref('https://me:pwd@it-tools.tech:3000/url-parser?key1=value&key2=value2#the-hash'); + +const urlParsed = computed(() => withDefaultOnError(() => new URL(urlToParse.value), undefined)); +const urlValidationRules = [ + { + validator: (value: string) => isNotThrowing(() => new URL(value)), + message: 'Invalid url', + }, +]; + +const properties: { title: string; key: keyof URL }[] = [ + { title: 'Protocol', key: 'protocol' }, + { title: 'Username', key: 'username' }, + { title: 'Password', key: 'password' }, + { title: 'Hostname', key: 'hostname' }, + { title: 'Port', key: 'port' }, + { title: 'Path', key: 'pathname' }, + { title: 'Params', key: 'search' }, +]; +</script> + <template> <c-card> <c-input-text @@ -10,7 +37,7 @@ <n-divider /> - <input-copyable + <InputCopyable v-for="{ title, key } in properties" :key="key" :label="title" @@ -33,39 +60,12 @@ <icon-mdi-arrow-right-bottom /> </div> - <input-copyable :value="k" readonly /> - <input-copyable :value="v" readonly /> + <InputCopyable :value="k" readonly /> + <InputCopyable :value="v" readonly /> </div> </c-card> </template> -<script setup lang="ts"> -import { isNotThrowing } from '@/utils/boolean'; -import { withDefaultOnError } from '@/utils/defaults'; -import { computed, ref } from 'vue'; -import InputCopyable from '../../components/InputCopyable.vue'; - -const urlToParse = ref('https://me:pwd@it-tools.tech:3000/url-parser?key1=value&key2=value2#the-hash'); - -const urlParsed = computed(() => withDefaultOnError(() => new URL(urlToParse.value), undefined)); -const urlValidationRules = [ - { - validator: (value: string) => isNotThrowing(() => new URL(value)), - message: 'Invalid url', - }, -]; - -const properties: { title: string; key: keyof URL }[] = [ - { title: 'Protocol', key: 'protocol' }, - { title: 'Username', key: 'username' }, - { title: 'Password', key: 'password' }, - { title: 'Hostname', key: 'hostname' }, - { title: 'Port', key: 'port' }, - { title: 'Path', key: 'pathname' }, - { title: 'Params', key: 'search' }, -]; -</script> - <style lang="less" scoped> .n-input-group-label { text-align: right; diff --git a/src/tools/user-agent-parser/user-agent-parser.types.ts b/src/tools/user-agent-parser/user-agent-parser.types.ts index 6c2720b..37dbe9f 100644 --- a/src/tools/user-agent-parser/user-agent-parser.types.ts +++ b/src/tools/user-agent-parser/user-agent-parser.types.ts @@ -1,12 +1,12 @@ import type { Component } from 'vue'; -import { UAParser } from 'ua-parser-js'; +import type { UAParser } from 'ua-parser-js'; -export type UserAgentResultSection = { - heading: string; - icon?: Component; +export interface UserAgentResultSection { + heading: string + icon?: Component content: { - label: string; - getValue: (blocks?: UAParser.IResult) => string | undefined; - undefinedFallback?: string; - }[]; -}; + label: string + getValue: (blocks?: UAParser.IResult) => string | undefined + undefinedFallback?: string + }[] +} diff --git a/src/tools/user-agent-parser/user-agent-parser.vue b/src/tools/user-agent-parser/user-agent-parser.vue index 8a3435d..02d1e56 100644 --- a/src/tools/user-agent-parser/user-agent-parser.vue +++ b/src/tools/user-agent-parser/user-agent-parser.vue @@ -1,36 +1,21 @@ -<template> - <div> - <n-form-item label="User agent string"> - <n-input - v-model:value="ua" - type="textarea" - placeholder="Put your user-agent here..." - clearable - :autosize="{ minRows: 2 }" - /> - </n-form-item> - - <user-agent-result-cards :user-agent-info="userAgentInfo" :sections="sections" /> - </div> -</template> - <script setup lang="ts"> import { computed, ref } from 'vue'; import { UAParser } from 'ua-parser-js'; -import { withDefaultOnError } from '@/utils/defaults'; import { Adjustments, Browser, Cpu, Devices, Engine } from '@vicons/tabler'; import UserAgentResultCards from './user-agent-result-cards.vue'; import type { UserAgentResultSection } from './user-agent-parser.types'; +import { withDefaultOnError } from '@/utils/defaults'; const ua = ref(navigator.userAgent as string); // If not input in the ua field is present return an empty object of type UAParser.IResult because otherwise // UAParser returns the values for the current Browser. This is confusing because results are shown for an empty // UA field value. -const getUserAgentInfo = (userAgent: string) => - userAgent.trim().length > 0 +function getUserAgentInfo(userAgent: string) { + return userAgent.trim().length > 0 ? UAParser(userAgent.trim()) : ({ ua: '', browser: {}, cpu: {}, device: {}, engine: {}, os: {} } as UAParser.IResult); +} const userAgentInfo = computed(() => withDefaultOnError(() => getUserAgentInfo(ua.value), undefined)); const sections: UserAgentResultSection[] = [ @@ -40,12 +25,12 @@ const sections: UserAgentResultSection[] = [ content: [ { label: 'Name', - getValue: (block) => block?.browser.name, + getValue: block => block?.browser.name, undefinedFallback: 'No browser name available', }, { label: 'Version', - getValue: (block) => block?.browser.version, + getValue: block => block?.browser.version, undefinedFallback: 'No browser version available', }, ], @@ -56,12 +41,12 @@ const sections: UserAgentResultSection[] = [ content: [ { label: 'Name', - getValue: (block) => block?.engine.name, + getValue: block => block?.engine.name, undefinedFallback: 'No engine name available', }, { label: 'Version', - getValue: (block) => block?.engine.version, + getValue: block => block?.engine.version, undefinedFallback: 'No engine version available', }, ], @@ -72,12 +57,12 @@ const sections: UserAgentResultSection[] = [ content: [ { label: 'Name', - getValue: (block) => block?.os.name, + getValue: block => block?.os.name, undefinedFallback: 'No OS name available', }, { label: 'Version', - getValue: (block) => block?.os.version, + getValue: block => block?.os.version, undefinedFallback: 'No OS version available', }, ], @@ -88,17 +73,17 @@ const sections: UserAgentResultSection[] = [ content: [ { label: 'Model', - getValue: (block) => block?.device.model, + getValue: block => block?.device.model, undefinedFallback: 'No device model available', }, { label: 'Type', - getValue: (block) => block?.device.type, + getValue: block => block?.device.type, undefinedFallback: 'No device type available', }, { label: 'Vendor', - getValue: (block) => block?.device.vendor, + getValue: block => block?.device.vendor, undefinedFallback: 'No device vendor available', }, ], @@ -109,7 +94,7 @@ const sections: UserAgentResultSection[] = [ content: [ { label: 'Architecture', - getValue: (block) => block?.cpu.architecture, + getValue: block => block?.cpu.architecture, undefinedFallback: 'No CPU architecture available', }, ], @@ -117,4 +102,18 @@ const sections: UserAgentResultSection[] = [ ]; </script> -<style lang="less" scoped></style> +<template> + <div> + <n-form-item label="User agent string"> + <n-input + v-model:value="ua" + type="textarea" + placeholder="Put your user-agent here..." + clearable + :autosize="{ minRows: 2 }" + /> + </n-form-item> + + <UserAgentResultCards :user-agent-info="userAgentInfo" :sections="sections" /> + </div> +</template> diff --git a/src/tools/user-agent-parser/user-agent-result-cards.vue b/src/tools/user-agent-parser/user-agent-result-cards.vue index 20226c1..b3901ac 100644 --- a/src/tools/user-agent-parser/user-agent-result-cards.vue +++ b/src/tools/user-agent-parser/user-agent-result-cards.vue @@ -1,3 +1,15 @@ +<script setup lang="ts"> +import { toRefs } from 'vue'; +import type { UAParser } from 'ua-parser-js'; +import type { UserAgentResultSection } from './user-agent-parser.types'; + +const props = defineProps<{ + userAgentInfo?: UAParser.IResult + sections: UserAgentResultSection[] +}>(); +const { userAgentInfo, sections } = toRefs(props); +</script> + <template> <div> <n-grid :x-gap="12" :y-gap="8" cols="1 s:2" responsive="screen"> @@ -34,15 +46,3 @@ </n-grid> </div> </template> - -<script setup lang="ts"> -import { toRefs } from 'vue'; -import { UAParser } from 'ua-parser-js'; -import type { UserAgentResultSection } from './user-agent-parser.types'; - -const props = defineProps<{ - userAgentInfo?: UAParser.IResult; - sections: UserAgentResultSection[]; -}>(); -const { userAgentInfo, sections } = toRefs(props); -</script> diff --git a/src/tools/uuid-generator/uuid-generator.vue b/src/tools/uuid-generator/uuid-generator.vue index de9cbf2..f1265e1 100644 --- a/src/tools/uuid-generator/uuid-generator.vue +++ b/src/tools/uuid-generator/uuid-generator.vue @@ -1,3 +1,17 @@ +<script setup lang="ts"> +import { v4 as generateUUID } from 'uuid'; +import { useCopy } from '@/composable/copy'; +import { computedRefreshable } from '@/composable/computedRefreshable'; + +const count = useStorage('uuid-generator:quantity', 1); + +const [uuids, refreshUUIDs] = computedRefreshable(() => + Array.from({ length: count.value }, () => generateUUID()).join('\n'), +); + +const { copy } = useCopy({ source: uuids, text: 'UUIDs copied to the clipboard' }); +</script> + <template> <div> <div flex items-center justify-center gap-3> @@ -20,22 +34,12 @@ /> <div flex justify-center gap-3> - <c-button autofocus @click="copy"> Copy </c-button> - <c-button @click="refreshUUIDs"> Refresh </c-button> + <c-button autofocus @click="copy"> + Copy + </c-button> + <c-button @click="refreshUUIDs"> + Refresh + </c-button> </div> </div> </template> - -<script setup lang="ts"> -import { useCopy } from '@/composable/copy'; -import { v4 as generateUUID } from 'uuid'; -import { computedRefreshable } from '@/composable/computedRefreshable'; - -const count = useStorage('uuid-generator:quantity', 1); - -const [uuids, refreshUUIDs] = computedRefreshable(() => - Array.from({ length: count.value }, () => generateUUID()).join('\n'), -); - -const { copy } = useCopy({ source: uuids, text: 'UUIDs copied to the clipboard' }); -</script> diff --git a/src/tools/yaml-to-json-converter/yaml-to-json.e2e.spec.ts b/src/tools/yaml-to-json-converter/yaml-to-json.e2e.spec.ts index 10db449..7b2a2d1 100644 --- a/src/tools/yaml-to-json-converter/yaml-to-json.e2e.spec.ts +++ b/src/tools/yaml-to-json-converter/yaml-to-json.e2e.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.describe('Tool - Yaml to json', () => { test.beforeEach(async ({ page }) => { diff --git a/src/tools/yaml-to-json-converter/yaml-to-json.vue b/src/tools/yaml-to-json-converter/yaml-to-json.vue index c066bdd..39c9297 100644 --- a/src/tools/yaml-to-json-converter/yaml-to-json.vue +++ b/src/tools/yaml-to-json-converter/yaml-to-json.vue @@ -1,25 +1,15 @@ -<template> - <format-transformer - input-label="Your YAML" - input-placeholder="Paste your yaml here..." - output-label="JSON from your YAML" - output-language="json" - :input-validation-rules="rules" - :transformer="transformer" - /> -</template> - <script setup lang="ts"> +import { parse as parseYaml } from 'yaml'; import type { UseValidationRule } from '@/composable/validation'; import { isNotThrowing } from '@/utils/boolean'; import { withDefaultOnError } from '@/utils/defaults'; -import { parse as parseYaml } from 'yaml'; -const transformer = (value: string) => - withDefaultOnError(() => { +function transformer(value: string) { + return withDefaultOnError(() => { const obj = parseYaml(value); return obj ? JSON.stringify(obj, null, 3) : ''; }, ''); +} const rules: UseValidationRule<string>[] = [ { @@ -29,4 +19,13 @@ const rules: UseValidationRule<string>[] = [ ]; </script> -<style lang="less" scoped></style> +<template> + <format-transformer + input-label="Your YAML" + input-placeholder="Paste your yaml here..." + output-label="JSON from your YAML" + output-language="json" + :input-validation-rules="rules" + :transformer="transformer" + /> +</template> diff --git a/src/ui/c-alert/c-alert.demo.vue b/src/ui/c-alert/c-alert.demo.vue index 5d8d1f2..546c785 100644 --- a/src/ui/c-alert/c-alert.demo.vue +++ b/src/ui/c-alert/c-alert.demo.vue @@ -1,3 +1,7 @@ +<script lang="ts" setup> +const variants = ['warning'] as const; +</script> + <template> <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 @@ -5,7 +9,3 @@ odio! </c-alert> </template> - -<script lang="ts" setup> -const variants = ['warning'] as const; -</script> diff --git a/src/ui/c-alert/c-alert.theme.ts b/src/ui/c-alert/c-alert.theme.ts index b974c37..36d5d34 100644 --- a/src/ui/c-alert/c-alert.theme.ts +++ b/src/ui/c-alert/c-alert.theme.ts @@ -2,7 +2,6 @@ import { darken } from '../color/color.models'; import { defineThemes } from '../theme/theme.models'; import { appThemes } from '../theme/themes'; -// eslint-disable-next-line import WarningIcon from '~icons/mdi/alert-circle-outline'; export const { useTheme } = defineThemes({ diff --git a/src/ui/c-alert/c-alert.vue b/src/ui/c-alert/c-alert.vue index 1fedbb0..607acb9 100644 --- a/src/ui/c-alert/c-alert.vue +++ b/src/ui/c-alert/c-alert.vue @@ -1,3 +1,13 @@ +<script lang="ts" setup> +import { useTheme } from './c-alert.theme'; + +const props = withDefaults(defineProps<{ type?: 'warning' }>(), { type: 'warning' }); +const { type } = toRefs(props); + +const theme = useTheme(); +const variantTheme = computed(() => theme.value[type.value]); +</script> + <template> <div class="c-alert" flex items-center b-rd-4px pa-5> <div class="c-alert--icon" mr-4 text-40px op-60> @@ -12,16 +22,6 @@ </div> </template> -<script lang="ts" setup> -import { useTheme } from './c-alert.theme'; - -const props = withDefaults(defineProps<{ type?: 'warning' }>(), { type: 'warning' }); -const { type } = toRefs(props); - -const theme = useTheme(); -const variantTheme = computed(() => theme.value[type.value]); -</script> - <style lang="less" scoped> .c-alert { background-color: v-bind('variantTheme.backgroundColor'); diff --git a/src/ui/c-button/c-button.demo.vue b/src/ui/c-button/c-button.demo.vue index 48576f6..b9d011a 100644 --- a/src/ui/c-button/c-button.demo.vue +++ b/src/ui/c-button/c-button.demo.vue @@ -1,3 +1,11 @@ +<script lang="ts" setup> +import _ from 'lodash'; + +const buttonVariants = ['basic', 'text'] as const; +const buttonTypes = ['default', 'primary', 'warning', 'error'] as const; +const buttonSizes = ['small', 'medium', 'large'] as const; +</script> + <template> <div v-for="buttonVariant of buttonVariants" :key="buttonVariant"> <h2>{{ _.capitalize(buttonVariant) }}</h2> @@ -40,13 +48,3 @@ </div> </div> </template> - -<script lang="ts" setup> -import _ from 'lodash'; - -const buttonVariants = ['basic', 'text'] as const; -const buttonTypes = ['default', 'primary', 'warning', 'error'] as const; -const buttonSizes = ['small', 'medium', 'large'] as const; -</script> - -<style lang="less" scoped></style> diff --git a/src/ui/c-button/c-button.theme.ts b/src/ui/c-button/c-button.theme.ts index e2e1591..926cd11 100644 --- a/src/ui/c-button/c-button.theme.ts +++ b/src/ui/c-button/c-button.theme.ts @@ -2,7 +2,7 @@ import { darken, lighten } from '../color/color.models'; import { defineThemes } from '../theme/theme.models'; import { appThemes } from '../theme/themes'; -const createState = ({ +function createState({ textColor, backgroundColor, hoverBackground, @@ -10,20 +10,22 @@ const createState = ({ pressedBackground, pressedTextColor = textColor, }: { - textColor: string; - backgroundColor: string; - hoverBackground: string; - hoveredTextColor?: string; - pressedBackground: string; - pressedTextColor?: string; -}) => ({ - textColor, - backgroundColor, - hover: { textColor: hoveredTextColor, backgroundColor: hoverBackground }, - pressed: { textColor: pressedTextColor, backgroundColor: pressedBackground }, -}); + textColor: string + backgroundColor: string + hoverBackground: string + hoveredTextColor?: string + pressedBackground: string + pressedTextColor?: string +}) { + return { + textColor, + backgroundColor, + hover: { textColor: hoveredTextColor, backgroundColor: hoverBackground }, + pressed: { textColor: pressedTextColor, backgroundColor: pressedBackground }, + }; +} -const createTheme = ({ style }: { style: 'light' | 'dark' }) => { +function createTheme({ style }: { style: 'light' | 'dark' }) { const theme = appThemes[style]; return { @@ -95,7 +97,7 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => { }), }, }; -}; +} export const { useTheme } = defineThemes({ dark: createTheme({ style: 'dark' }), diff --git a/src/ui/c-button/c-button.vue b/src/ui/c-button/c-button.vue index 24b91b8..06a4786 100644 --- a/src/ui/c-button/c-button.vue +++ b/src/ui/c-button/c-button.vue @@ -1,31 +1,18 @@ -<template> - <component - :is="tag" - :href="href ?? to" - class="c-button" - :class="{ disabled, round, circle }" - :to="to" - @click="handleClick" - > - <slot /> - </component> -</template> - <script lang="ts" setup> import type { RouteLocationRaw } from 'vue-router'; -import { useTheme } from './c-button.theme'; import { useAppTheme } from '../theme/themes'; +import { useTheme } from './c-button.theme'; const props = withDefaults( defineProps<{ - type?: 'default' | 'primary' | 'warning' | 'error'; - variant?: 'basic' | 'text'; - disabled?: boolean; - round?: boolean; - circle?: boolean; - href?: string; - to?: RouteLocationRaw; - size?: 'small' | 'medium' | 'large'; + type?: 'default' | 'primary' | 'warning' | 'error' + variant?: 'basic' | 'text' + disabled?: boolean + round?: boolean + circle?: boolean + href?: string + to?: RouteLocationRaw + size?: 'small' | 'medium' | 'large' }>(), { type: 'default', @@ -38,10 +25,10 @@ const props = withDefaults( size: 'medium', }, ); -const { variant, disabled, round, circle, href, type, to, size: sizeName } = toRefs(props); - const emits = defineEmits(['click']); +const { variant, disabled, round, circle, href, type, to, size: sizeName } = toRefs(props); + function handleClick(event: MouseEvent) { if (!disabled.value) { emits('click', event); @@ -64,6 +51,19 @@ const appTheme = useAppTheme(); const size = computed(() => theme.value.size[sizeName.value]); </script> +<template> + <component + :is="tag" + :href="href ?? to" + class="c-button" + :class="{ disabled, round, circle }" + :to="to" + @click="handleClick" + > + <slot /> + </component> +</template> + <style lang="less" scoped> .c-button { line-height: 1; diff --git a/src/ui/c-card/c-card.demo.vue b/src/ui/c-card/c-card.demo.vue index 6d81ee6..2a6fb9d 100644 --- a/src/ui/c-card/c-card.demo.vue +++ b/src/ui/c-card/c-card.demo.vue @@ -9,5 +9,3 @@ </c-card> </div> </template> - -<script lang="ts" setup></script> diff --git a/src/ui/c-card/c-card.vue b/src/ui/c-card/c-card.vue index 11d86fd..739e657 100644 --- a/src/ui/c-card/c-card.vue +++ b/src/ui/c-card/c-card.vue @@ -1,17 +1,8 @@ -<template> - <div class="c-card"> - <div v-if="title" class="c-card-title"> - {{ title }} - </div> - <slot /> - </div> -</template> - <script lang="ts" setup> import { useTheme } from './c-card.theme'; const props = defineProps<{ - title?: string; + title?: string }>(); const { title } = toRefs(props); @@ -19,6 +10,15 @@ const { title } = toRefs(props); const theme = useTheme(); </script> +<template> + <div class="c-card"> + <div v-if="title" class="c-card-title"> + {{ title }} + </div> + <slot /> + </div> +</template> + <style lang="less" scoped> .c-card { background-color: v-bind('theme.backgroundColor'); diff --git a/src/ui/c-input-text/c-input-text.demo.vue b/src/ui/c-input-text/c-input-text.demo.vue index 5a5fa99..b027787 100644 --- a/src/ui/c-input-text/c-input-text.demo.vue +++ b/src/ui/c-input-text/c-input-text.demo.vue @@ -1,3 +1,19 @@ +<script lang="ts" setup> +import { useValidation } from '@/composable/validation'; + +const value = ref('value'); +const valueLong = ref( + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?', +); + +const validationRules = [{ message: 'Length must be > 10', validator: (value: string) => value.length > 10 }]; + +const validation = useValidation({ + source: value, + rules: validationRules, +}); +</script> + <template> <h2>Default</h2> @@ -58,19 +74,3 @@ clearable /> </template> - -<script lang="ts" setup> -import { useValidation } from '@/composable/validation'; - -const value = ref('value'); -const valueLong = ref( - 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?', -); - -const validationRules = [{ message: 'Length must be > 10', validator: (value: string) => value.length > 10 }]; - -const validation = useValidation({ - source: value, - rules: validationRules, -}); -</script> diff --git a/src/ui/c-input-text/c-input-text.test.ts b/src/ui/c-input-text/c-input-text.test.ts index 69f4046..2d1908d 100644 --- a/src/ui/c-input-text/c-input-text.test.ts +++ b/src/ui/c-input-text/c-input-text.test.ts @@ -1,9 +1,9 @@ -import { describe, expect, it, beforeEach } from 'vitest'; -import { shallowMount, mount } from '@vue/test-utils'; -import { setActivePinia, createPinia } from 'pinia'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { mount, shallowMount } from '@vue/test-utils'; +import { createPinia, setActivePinia } from 'pinia'; import _ from 'lodash'; -import { useValidation } from '@/composable/validation'; import CInputText from './c-input-text.vue'; +import { useValidation } from '@/composable/validation'; describe('CInputText', () => { beforeEach(() => { diff --git a/src/ui/c-input-text/c-input-text.vue b/src/ui/c-input-text/c-input-text.vue index cd5f067..90d7152 100644 --- a/src/ui/c-input-text/c-input-text.vue +++ b/src/ui/c-input-text/c-input-text.vue @@ -1,95 +1,35 @@ -<template> - <div - class="c-input-text" - :class="{ disabled, error: !validation.isValid, 'label-left': labelPosition === 'left', multiline }" - > - <label v-if="label" :for="id" class="label"> {{ label }} </label> - - <div class="feedback-wrapper"> - <div ref="inputWrapperRef" class="input-wrapper"> - <slot name="prefix" /> - - <textarea - v-if="multiline" - :id="id" - ref="textareaRef" - v-model="value" - class="input" - :placeholder="placeholder" - :readonly="readonly" - :disabled="disabled" - :data-test-id="testId" - :autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)" - :autocomplete="autocomplete ?? (rawText ? 'off' : undefined)" - :autocorrect="autocorrect ?? (rawText ? 'off' : undefined)" - :spellcheck="spellcheck ?? (rawText ? false : undefined)" - :rows="rows" - /> - - <input - v-else - :id="id" - v-model="value" - :type="htmlInputType" - class="input" - size="1" - :placeholder="placeholder" - :readonly="readonly" - :disabled="disabled" - :data-test-id="testId" - :autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)" - :autocomplete="autocomplete ?? (rawText ? 'off' : undefined)" - :autocorrect="autocorrect ?? (rawText ? 'off' : undefined)" - :spellcheck="spellcheck ?? (rawText ? false : undefined)" - /> - - <c-button v-if="clearable && value" variant="text" circle size="small" @click="value = ''"> - <icon-mdi-close /> - </c-button> - - <c-button v-if="type === 'password'" variant="text" circle size="small" @click="showPassword = !showPassword"> - <icon-mdi-eye v-if="!showPassword" /> - <icon-mdi-eye-off v-if="showPassword" /> - </c-button> - <slot name="suffix" /> - </div> - <span v-if="!validation.isValid" class="feedback"> {{ validation.message }} </span> - </div> - </div> -</template> - <script lang="ts" setup> -import { generateRandomId } from '@/utils/random'; -import { useValidation, type UseValidationRule } from '@/composable/validation'; import type { Ref } from 'vue'; -import { useTheme } from './c-input-text.theme'; import { useAppTheme } from '../theme/themes'; +import { useTheme } from './c-input-text.theme'; +import { generateRandomId } from '@/utils/random'; +import { type UseValidationRule, useValidation } from '@/composable/validation'; const props = withDefaults( defineProps<{ - value?: string; - id?: string; - placeholder?: string; - label?: string; - readonly?: boolean; - disabled?: boolean; - validationRules?: UseValidationRule<string>[]; - validationWatch?: Ref<unknown>[]; - validation?: ReturnType<typeof useValidation>; - labelPosition?: 'top' | 'left'; - labelWidth?: string; - labelAlign?: 'left' | 'right'; - clearable?: boolean; - testId?: string; - autocapitalize?: 'none' | 'sentences' | 'words' | 'characters' | 'on' | 'off' | string; - autocomplete?: 'on' | 'off' | string; - autocorrect?: 'on' | 'off' | string; - spellcheck?: 'true' | 'false' | boolean; - rawText?: boolean; - type?: 'text' | 'password'; - multiline?: boolean; - rows?: number | string; - autosize?: boolean; + value?: string + id?: string + placeholder?: string + label?: string + readonly?: boolean + disabled?: boolean + validationRules?: UseValidationRule<string>[] + validationWatch?: Ref<unknown>[] + validation?: ReturnType<typeof useValidation> + labelPosition?: 'top' | 'left' + labelWidth?: string + labelAlign?: 'left' | 'right' + clearable?: boolean + testId?: string + autocapitalize?: 'none' | 'sentences' | 'words' | 'characters' | 'on' | 'off' | string + autocomplete?: 'on' | 'off' | string + autocorrect?: 'on' | 'off' | string + spellcheck?: 'true' | 'false' | boolean + rawText?: boolean + type?: 'text' | 'password' + multiline?: boolean + rows?: number | string + autosize?: boolean }>(), { value: '', @@ -123,9 +63,9 @@ const showPassword = ref(false); const { id, placeholder, label, validationRules, labelPosition, labelWidth, labelAlign, autosize } = toRefs(props); -const validation = - props.validation ?? - useValidation({ +const validation + = props.validation + ?? useValidation({ rules: validationRules, source: value, watch: props.validationWatch, @@ -170,6 +110,66 @@ const htmlInputType = computed(() => { }); </script> +<template> + <div + class="c-input-text" + :class="{ disabled, 'error': !validation.isValid, 'label-left': labelPosition === 'left', multiline }" + > + <label v-if="label" :for="id" class="label"> {{ label }} </label> + + <div class="feedback-wrapper"> + <div ref="inputWrapperRef" class="input-wrapper"> + <slot name="prefix" /> + + <textarea + v-if="multiline" + :id="id" + ref="textareaRef" + v-model="value" + class="input" + :placeholder="placeholder" + :readonly="readonly" + :disabled="disabled" + :data-test-id="testId" + :autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)" + :autocomplete="autocomplete ?? (rawText ? 'off' : undefined)" + :autocorrect="autocorrect ?? (rawText ? 'off' : undefined)" + :spellcheck="spellcheck ?? (rawText ? false : undefined)" + :rows="rows" + /> + + <input + v-else + :id="id" + v-model="value" + :type="htmlInputType" + class="input" + size="1" + :placeholder="placeholder" + :readonly="readonly" + :disabled="disabled" + :data-test-id="testId" + :autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)" + :autocomplete="autocomplete ?? (rawText ? 'off' : undefined)" + :autocorrect="autocorrect ?? (rawText ? 'off' : undefined)" + :spellcheck="spellcheck ?? (rawText ? false : undefined)" + > + + <c-button v-if="clearable && value" variant="text" circle size="small" @click="value = ''"> + <icon-mdi-close /> + </c-button> + + <c-button v-if="type === 'password'" variant="text" circle size="small" @click="showPassword = !showPassword"> + <icon-mdi-eye v-if="!showPassword" /> + <icon-mdi-eye-off v-if="showPassword" /> + </c-button> + <slot name="suffix" /> + </div> + <span v-if="!validation.isValid" class="feedback"> {{ validation.message }} </span> + </div> + </div> +</template> + <style lang="less" scoped> .c-input-text { display: inline-flex; diff --git a/src/ui/c-link/c-link.demo.vue b/src/ui/c-link/c-link.demo.vue index a655f11..2573e9f 100644 --- a/src/ui/c-link/c-link.demo.vue +++ b/src/ui/c-link/c-link.demo.vue @@ -1,12 +1,12 @@ +<script lang="ts" setup> +import CLink from './c-link.vue'; +</script> + <template> <div> <h2>Default</h2> - <c-link mx-1> Link </c-link> + <CLink mx-1> + Link + </CLink> </div> </template> - -<script lang="ts" setup> -import CLink from './c-link.vue'; -</script> - -<style lang="less" scoped></style> diff --git a/src/ui/c-link/c-link.vue b/src/ui/c-link/c-link.vue index a7d1b83..828d56f 100644 --- a/src/ui/c-link/c-link.vue +++ b/src/ui/c-link/c-link.vue @@ -1,16 +1,10 @@ -<template> - <component :is="tag" :href="href ?? to" class="c-link" :to="to"> - <slot /> - </component> -</template> - <script lang="ts" setup> -import { RouterLink, type RouteLocationRaw } from 'vue-router'; +import { type RouteLocationRaw, RouterLink } from 'vue-router'; import { useTheme } from './c-link.theme'; const props = defineProps<{ - href?: string; - to?: RouteLocationRaw; + href?: string + to?: RouteLocationRaw }>(); const { href, to } = toRefs(props); @@ -27,6 +21,12 @@ const tag = computed(() => { }); </script> +<template> + <component :is="tag" :href="href ?? to" class="c-link" :to="to"> + <slot /> + </component> +</template> + <style lang="less" scoped> .c-link { line-height: inherit; diff --git a/src/ui/color/color.models.test.ts b/src/ui/color/color.models.test.ts index dc59fa8..256c100 100644 --- a/src/ui/color/color.models.test.ts +++ b/src/ui/color/color.models.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from 'vitest'; +import { describe, expect, test } from 'vitest'; import { darken, lighten, setOpacity } from './color.models'; describe('color models', () => { diff --git a/src/ui/demo/demo-wrapper.vue b/src/ui/demo/demo-wrapper.vue index 8d4bae0..c4d3604 100644 --- a/src/ui/demo/demo-wrapper.vue +++ b/src/ui/demo/demo-wrapper.vue @@ -1,3 +1,12 @@ +<script lang="ts" setup> +import _ from 'lodash'; +import { demoRoutes } from './demo.routes'; + +const route = useRoute(); + +const componentName = computed(() => _.startCase(String(route.name).replace(/^c-/, ''))); +</script> + <template> <div mt-2 w-full p-8> <h1>c-lib components</h1> @@ -25,14 +34,3 @@ </div> </div> </template> - -<script lang="ts" setup> -import _ from 'lodash'; -import { demoRoutes } from './demo.routes'; - -const route = useRoute(); - -const componentName = computed(() => _.startCase(String(route.name).replace(/^c-/, ''))); -</script> - -<style lang="less" scoped></style> diff --git a/src/utils/base64.ts b/src/utils/base64.ts index 44fda1e..16912ee 100644 --- a/src/utils/base64.ts +++ b/src/utils/base64.ts @@ -6,7 +6,7 @@ function textToBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: bool } function base64ToText(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) { - if (!isValidBase64(str, { makeUrlSafe: makeUrlSafe })) { + if (!isValidBase64(str, { makeUrlSafe })) { throw new Error('Incorrect base64 string'); } @@ -17,7 +17,8 @@ function base64ToText(str: string, { makeUrlSafe = false }: { makeUrlSafe?: bool try { return window.atob(cleanStr); - } catch (_) { + } + catch (_) { throw new Error('Incorrect base64 string'); } } @@ -37,7 +38,8 @@ function isValidBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boo return removePotentialPadding(window.btoa(window.atob(cleanStr))) === cleanStr; } return window.btoa(window.atob(cleanStr)) === cleanStr; - } catch (err) { + } + catch (err) { return false; } } diff --git a/src/utils/boolean.test.ts b/src/utils/boolean.test.ts index 52bda9e..07daa05 100644 --- a/src/utils/boolean.test.ts +++ b/src/utils/boolean.test.ts @@ -8,7 +8,7 @@ describe('boolean utils', () => { expect(isNotThrowing(_.noop)).to.eql(true); expect( isNotThrowing(() => { - throw new Error(); + throw new Error('message'); }), ).to.eql(false); }); diff --git a/src/utils/boolean.ts b/src/utils/boolean.ts index cf10b37..8dca5e9 100644 --- a/src/utils/boolean.ts +++ b/src/utils/boolean.ts @@ -4,7 +4,8 @@ function isNotThrowing(cb: () => unknown): boolean { try { cb(); return true; - } catch (_) { + } + catch (_) { return false; } } diff --git a/src/utils/convert.ts b/src/utils/convert.ts index c8c325f..c897543 100644 --- a/src/utils/convert.ts +++ b/src/utils/convert.ts @@ -7,5 +7,5 @@ export function formatBytes(bytes: number, decimals = 2) { const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]; + return `${parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`; } diff --git a/src/utils/defaults.test.ts b/src/utils/defaults.test.ts index b322968..5001798 100644 --- a/src/utils/defaults.test.ts +++ b/src/utils/defaults.test.ts @@ -9,7 +9,7 @@ describe('defaults util', () => { expect( withDefaultOnError(() => { - throw ''; + throw new Error('message'); }, 'default'), ).to.eql('default'); }); diff --git a/src/utils/defaults.ts b/src/utils/defaults.ts index 1e52b49..c988af5 100644 --- a/src/utils/defaults.ts +++ b/src/utils/defaults.ts @@ -3,7 +3,8 @@ export { withDefaultOnError, withDefaultOnErrorAsync }; function withDefaultOnError<A, B>(cb: () => A, defaultValue: B): A | B { try { return cb(); - } catch (_) { + } + catch (_) { return defaultValue; } } @@ -11,7 +12,8 @@ function withDefaultOnError<A, B>(cb: () => A, defaultValue: B): A | B { async function withDefaultOnErrorAsync<A, B>(cb: () => A, defaultValue: B): Promise<Awaited<A> | B> { try { return await cb(); - } catch (_) { + } + catch (_) { return defaultValue; } } diff --git a/src/utils/error.test.ts b/src/utils/error.test.ts index 0272804..62bf272 100644 --- a/src/utils/error.test.ts +++ b/src/utils/error.test.ts @@ -6,6 +6,7 @@ describe('error util', () => { it('get an error message if the callback throws, undefined instead', () => { expect( getErrorMessageIfThrows(() => { + // eslint-disable-next-line no-throw-literal throw 'message'; }), ).to.equal('message'); @@ -18,11 +19,11 @@ describe('error util', () => { expect( getErrorMessageIfThrows(() => { + // eslint-disable-next-line no-throw-literal throw { message: 'message' }; }), ).to.equal('message'); - // eslint-disable-next-line @typescript-eslint/no-empty-function expect(getErrorMessageIfThrows(() => {})).to.equal(undefined); }); }); diff --git a/src/utils/error.ts b/src/utils/error.ts index 681db91..297edd9 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -6,7 +6,8 @@ function getErrorMessageIfThrows(cb: () => unknown) { try { cb(); return undefined; - } catch (err) { + } + catch (err) { if (_.isString(err)) { return err; } diff --git a/src/utils/macAddress.ts b/src/utils/macAddress.ts index 89f12d3..4488b32 100644 --- a/src/utils/macAddress.ts +++ b/src/utils/macAddress.ts @@ -1,5 +1,5 @@ -import { useValidation } from '@/composable/validation'; import type { Ref } from 'vue'; +import { useValidation } from '@/composable/validation'; const macAddressValidationRules = [ { diff --git a/src/utils/random.ts b/src/utils/random.ts index 3a13be5..02df947 100644 --- a/src/utils/random.ts +++ b/src/utils/random.ts @@ -5,14 +5,14 @@ const randFromArray = (array: unknown[]) => array[Math.floor(random() * array.le const randIntFromInterval = (min: number, max: number) => Math.floor(random() * (max - min) + min); // Durstenfeld shuffle -const shuffleArrayMutate = <T>(array: T[]): T[] => { +function shuffleArrayMutate<T>(array: T[]): T[] { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; -}; +} const shuffleArray = <T>(array: T[]): T[] => shuffleArrayMutate([...array]); |