chore(lint): switched to a better lint config

This commit is contained in:
Corentin Thomasset 2023-05-28 23:13:24 +02:00 committed by Corentin THOMASSET
parent 4d2b037dbe
commit 33c9b6643f
178 changed files with 4105 additions and 3371 deletions

View file

@ -1,39 +1,14 @@
/* eslint-env node */ /**
require('@rushstack/eslint-patch/modern-module-resolution'); * @type {import('eslint').Linter.Config}
*/
module.exports = { module.exports = {
root: true, root: true,
extends: [ extends: ['@antfu', './.eslintrc-auto-import.json', '@unocss'],
'plugin:vue/vue3-essential',
'eslint:recommended',
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier',
'plugin:import/recommended',
'./.eslintrc-auto-import.json',
'@unocss',
],
settings: {
'import/resolver': { typescript: { project: './tsconfig.app.json' } },
},
env: {
'vue/setup-compiler-macros': true,
},
rules: { rules: {
'vue/multi-word-component-names': ['off'], 'curly': ['error', 'all'],
'prettier/prettier': ['error'], '@typescript-eslint/semi': ['error', 'always'],
'import/no-duplicates': ['error', { considerQueryString: true }], '@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }], 'vue/no-empty-component-block': ['error'],
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-unresolved': ['error', { ignore: ['^virtual:'] }],
}, },
}; };

14
components.d.ts vendored
View file

@ -31,17 +31,13 @@ declare module '@vue/runtime-core' {
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default'] CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] 'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default']
'CInputText.theme': typeof import('./src/ui/c-input-text/c-input-text.theme.vue')['default']
CLink: typeof import('./src/ui/c-link/c-link.vue')['default'] CLink: typeof import('./src/ui/c-link/c-link.vue')['default']
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] 'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default'] CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default'] ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
ColoredCard: typeof import('./src/components/ColoredCard.vue')['default'] ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
copy: typeof import('./src/ui/c-input-text/c-input-text copy.vue')['default']
CopyableIpLike: typeof import('./src/tools/ipv4-subnet-calculator/copyable-ip-like.vue')['default']
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default']
'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default']
DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default']
DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default'] DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default']
DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default'] DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default']
@ -61,22 +57,16 @@ declare module '@vue/runtime-core' {
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
IconMdiCamera: typeof import('~icons/mdi/camera')['default'] IconMdiCamera: typeof import('~icons/mdi/camera')['default']
IconMdiCameraOutline: typeof import('~icons/mdi/camera-outline')['default']
IconMdiCameraVideoOff: typeof import('~icons/mdi/camera-video-off')['default']
IconMdiClose: typeof import('~icons/mdi/close')['default'] IconMdiClose: typeof import('~icons/mdi/close')['default']
IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
IconMdiDelete: typeof import('~icons/mdi/delete')['default']
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default'] IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
IconMdiDeleteOutlined: typeof import('~icons/mdi/delete-outlined')['default']
IconMdiDownload: typeof import('~icons/mdi/download')['default'] IconMdiDownload: typeof import('~icons/mdi/download')['default']
IconMdiEye: typeof import('~icons/mdi/eye')['default'] IconMdiEye: typeof import('~icons/mdi/eye')['default']
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
IconMdiPause: typeof import('~icons/mdi/pause')['default'] IconMdiPause: typeof import('~icons/mdi/pause')['default']
IconMdiPlay: typeof import('~icons/mdi/play')['default'] IconMdiPlay: typeof import('~icons/mdi/play')['default']
IconMdiRecord: typeof import('~icons/mdi/record')['default'] IconMdiRecord: typeof import('~icons/mdi/record')['default']
IconMdiRecordRec: typeof import('~icons/mdi/record-rec')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconMdiStopCircle: typeof import('~icons/mdi/stop-circle')['default']
IconMdiVideo: typeof import('~icons/mdi/video')['default'] IconMdiVideo: typeof import('~icons/mdi/video')['default']
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
@ -103,8 +93,6 @@ declare module '@vue/runtime-core' {
NAlert: typeof import('naive-ui')['NAlert'] NAlert: typeof import('naive-ui')['NAlert']
NAutoComplete: typeof import('naive-ui')['NAutoComplete'] NAutoComplete: typeof import('naive-ui')['NAutoComplete']
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox'] NCheckbox: typeof import('naive-ui')['NCheckbox']
NCode: typeof import('naive-ui')['NCode'] NCode: typeof import('naive-ui')['NCode']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
@ -116,7 +104,6 @@ declare module '@vue/runtime-core' {
NEllipsis: typeof import('naive-ui')['NEllipsis'] NEllipsis: typeof import('naive-ui')['NEllipsis']
NForm: typeof import('naive-ui')['NForm'] NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem'] NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi'] NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid'] NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1'] NH1: typeof import('naive-ui')['NH1']
@ -137,7 +124,6 @@ declare module '@vue/runtime-core' {
NScrollbar: typeof import('naive-ui')['NScrollbar'] NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect'] NSelect: typeof import('naive-ui')['NSelect']
NSlider: typeof import('naive-ui')['NSlider'] NSlider: typeof import('naive-ui')['NSlider']
NSpace: typeof import('naive-ui')['NSpace']
NStatistic: typeof import('naive-ui')['NStatistic'] NStatistic: typeof import('naive-ui')['NStatistic']
NSwitch: typeof import('naive-ui')['NSwitch'] NSwitch: typeof import('naive-ui')['NSwitch']
NTable: typeof import('naive-ui')['NTable'] NTable: typeof import('naive-ui')['NTable']

View file

@ -79,6 +79,7 @@
"yaml": "^2.2.1" "yaml": "^2.2.1"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.39.3",
"@iconify-json/mdi": "^1.1.50", "@iconify-json/mdi": "^1.1.50",
"@playwright/test": "^1.32.3", "@playwright/test": "^1.32.3",
"@rushstack/eslint-patch": "^1.2.0", "@rushstack/eslint-patch": "^1.2.0",
@ -100,18 +101,12 @@
"@vitejs/plugin-vue": "^2.3.4", "@vitejs/plugin-vue": "^2.3.4",
"@vitejs/plugin-vue-jsx": "^1.3.10", "@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/compiler-sfc": "^3.2.47", "@vue/compiler-sfc": "^3.2.47",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^10.0.0",
"@vue/runtime-core": "^3.2.47", "@vue/runtime-core": "^3.2.47",
"@vue/test-utils": "^2.3.2", "@vue/test-utils": "^2.3.2",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.1.3",
"c8": "^7.13.0", "c8": "^7.13.0",
"consola": "^3.0.2", "consola": "^3.0.2",
"eslint": "^8.38.0", "eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-vue": "^8.7.1",
"jsdom": "^19.0.0", "jsdom": "^19.0.0",
"less": "^4.1.3", "less": "^4.1.3",
"prettier": "^2.8.7", "prettier": "^2.8.7",

859
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useRoute, RouterView } from 'vue-router'; import { RouterView, useRoute } from 'vue-router';
import { darkTheme, NGlobalStyle, NMessageProvider, NNotificationProvider } from 'naive-ui'; import { NGlobalStyle, NMessageProvider, NNotificationProvider, darkTheme } from 'naive-ui';
import { darkThemeOverrides, lightThemeOverrides } from './themes'; import { darkThemeOverrides, lightThemeOverrides } from './themes';
import { layouts } from './layouts'; import { layouts } from './layouts';
import { useStyleStore } from './stores/style.store'; import { useStyleStore } from './stores/style.store';
@ -16,14 +16,14 @@ const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrid
<template> <template>
<n-config-provider :theme="theme" :theme-overrides="themeOverrides"> <n-config-provider :theme="theme" :theme-overrides="themeOverrides">
<n-global-style /> <NGlobalStyle />
<n-message-provider placement="bottom"> <NMessageProvider placement="bottom">
<n-notification-provider placement="bottom-right"> <NNotificationProvider placement="bottom-right">
<component :is="layout"> <component :is="layout">
<router-view /> <RouterView />
</component> </component>
</n-notification-provider> </NNotificationProvider>
</n-message-provider> </NMessageProvider>
</n-config-provider> </n-config-provider>
</template> </template>

View file

@ -1,3 +1,51 @@
<script setup lang="ts">
import { ChevronRight } from '@vicons/tabler';
import { useStorage } from '@vueuse/core';
import { useThemeVars } from 'naive-ui';
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);
const route = useRoute();
const makeLabel = (tool: Tool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name });
const makeIcon = (tool: Tool) => () => h(MenuIconItem, { tool });
const collapsedCategories = useStorage<Record<string, boolean>>(
'menu-tool-option:collapsed-categories',
{},
undefined,
{
deep: true,
serializer: {
read: v => (v ? JSON.parse(v) : null),
write: v => JSON.stringify(v),
},
},
);
function toggleCategoryCollapse({ name }: { name: string }) {
collapsedCategories.value[name] = !collapsedCategories.value[name];
}
const menuOptions = computed(() =>
toolsByCategory.value.map(({ name, components }) => ({
name,
isCollapsed: collapsedCategories.value[name],
tools: components.map(tool => ({
label: makeLabel(tool),
icon: makeIcon(tool),
key: tool.name,
})),
})),
);
const themeVars = useThemeVars();
</script>
<template> <template>
<div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name"> <div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name">
<n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })"> <n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })">
@ -14,7 +62,7 @@
<n-menu <n-menu
class="menu" class="menu"
:value="(route.name as string)" :value="route.name as string"
:collapsed-width="64" :collapsed-width="64"
:collapsed-icon-size="22" :collapsed-icon-size="22"
:options="tools" :options="tools"
@ -26,54 +74,6 @@
</div> </div>
</template> </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 { RouterLink, useRoute } from 'vue-router';
import MenuIconItem from './MenuIconItem.vue';
const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] });
const { toolsByCategory } = toRefs(props);
const route = useRoute();
const makeLabel = (tool: Tool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name });
const makeIcon = (tool: Tool) => () => h(MenuIconItem, { tool });
const collapsedCategories = useStorage<Record<string, boolean>>(
'menu-tool-option:collapsed-categories',
{},
undefined,
{
deep: true,
serializer: {
read: (v) => (v ? JSON.parse(v) : null),
write: (v) => JSON.stringify(v),
},
},
);
function toggleCategoryCollapse({ name }: { name: string }) {
collapsedCategories.value[name] = !collapsedCategories.value[name];
}
const menuOptions = computed(() =>
toolsByCategory.value.map(({ name, components }) => ({
name: name,
isCollapsed: collapsedCategories.value[name],
tools: components.map((tool) => ({
label: makeLabel(tool),
icon: makeIcon(tool),
key: tool.name,
})),
})),
);
const themeVars = useThemeVars();
</script>
<style scoped lang="less"> <style scoped lang="less">
.category-name { .category-name {
font-size: 0.93em; font-size: 0.93em;

View file

@ -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> <template>
<c-card class="colored-card"> <c-card class="colored-card">
<n-icon class="icon" size="40" :component="icon" /> <n-icon class="icon" size="40" :component="icon" />
@ -13,13 +20,6 @@
</c-card> </c-card>
</template> </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> <style lang="less" scoped>
.colored-card { .colored-card {
background: rgb(37, 99, 108); background: rgb(37, 99, 108);

View file

@ -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"> <script setup lang="ts">
import { FavoriteFilled } from '@vicons/material'; import { FavoriteFilled } from '@vicons/material';
import { computed, toRefs } from 'vue';
import { useToolStore } from '@/tools/tools.store'; import { useToolStore } from '@/tools/tools.store';
import type { Tool } from '@/tools/tools.types'; import type { Tool } from '@/tools/tools.types';
import { computed, toRefs } from 'vue';
const props = defineProps<{ tool: Tool }>();
const toolStore = useToolStore(); const toolStore = useToolStore();
const props = defineProps<{ tool: Tool }>();
const { tool } = toRefs(props); const { tool } = toRefs(props);
const isFavorite = computed(() => toolStore.isToolFavorite({ tool })); const isFavorite = computed(() => toolStore.isToolFavorite({ tool }));
@ -40,3 +24,20 @@ function toggleFavorite(event: MouseEvent) {
toolStore.addToolToFavorites({ tool }); toolStore.addToolToFavorites({ tool });
} }
</script> </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>

View file

@ -1,5 +1,40 @@
<script setup lang="ts">
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: _.identity,
inputValidationRules: () => [],
inputLabel: 'Input',
inputDefault: '',
inputPlaceholder: 'Input...',
outputLabel: 'Output',
outputLanguage: '',
},
);
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault }
= toRefs(props);
const inputElement = ref<typeof CInputText>();
const input = ref(inputDefault.value);
const output = computed(() => transformer.value(input.value));
</script>
<template> <template>
<c-input-text <CInputText
ref="inputElement" ref="inputElement"
v-model:value="input" v-model:value="input"
:placeholder="inputPlaceholder" :placeholder="inputPlaceholder"
@ -13,44 +48,9 @@
/> />
<div> <div>
<div mb-5px>{{ outputLabel }}</div> <div mb-5px>
{{ outputLabel }}
</div>
<textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" /> <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" />
</div> </div>
</template> </template>
<script setup lang="ts">
import type { UseValidationRule } from '@/composable/validation';
import _ from 'lodash';
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: _.identity,
inputValidationRules: () => [],
inputLabel: 'Input',
inputDefault: '',
inputPlaceholder: 'Input...',
outputLabel: 'Output',
outputLanguage: '',
},
);
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } =
toRefs(props);
const inputElement = ref<typeof CInputText>();
const input = ref(inputDefault.value);
const output = computed(() => transformer.value(input.value));
</script>
<style scoped></style>

View file

@ -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"> <script setup lang="ts">
import { useVModel, useClipboard } from '@vueuse/core'; import { useClipboard, useVModel } from '@vueuse/core';
import { ref } from 'vue'; import { ref } from 'vue';
const props = defineProps<{ value: string }>(); const props = defineProps<{ value: string }>();
@ -34,3 +19,18 @@ function onCopyClicked() {
}, 2000); }, 2000);
} }
</script> </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>

View file

@ -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"> <script setup lang="ts">
import type { Tool } from '@/tools/tools.types';
import { useThemeVars } from 'naive-ui'; import { useThemeVars } from 'naive-ui';
import { toRefs } from 'vue'; import { toRefs } from 'vue';
import type { Tool } from '@/tools/tools.types';
const props = defineProps<{ tool: Tool }>(); const props = defineProps<{ tool: Tool }>();
const { tool } = toRefs(props); const { tool } = toRefs(props);
@ -16,6 +9,13 @@ const { tool } = toRefs(props);
const theme = useThemeVars(); const theme = useThemeVars();
</script> </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> <style lang="less" scoped>
.menu-icon-item { .menu-icon-item {
position: relative; position: relative;

View file

@ -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> <template>
<n-layout has-sider> <n-layout has-sider>
<n-layout-sider <n-layout-sider
@ -19,15 +28,6 @@
</n-layout> </n-layout>
</template> </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> <style lang="less" scoped>
.overlay { .overlay {
position: absolute; position: absolute;

View file

@ -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> <template>
<n-tooltip trigger="hover"> <n-tooltip trigger="hover">
<template #trigger> <template #trigger>
@ -51,24 +69,6 @@
</n-tooltip> </n-tooltip>
</template> </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> <style lang="less" scoped>
.n-button { .n-button {
&:not(:last-child) { &:not(:last-child) {

View file

@ -1,14 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useFuzzySearch } from '@/composable/fuzzySearch';
import { useTracker } from '@/modules/tracker/tracker.services';
import { tools } from '@/tools';
import type { Tool } from '@/tools/tools.types';
import { SearchRound } from '@vicons/material'; import { SearchRound } from '@vicons/material';
import { useMagicKeys, whenever } from '@vueuse/core'; import { useMagicKeys, whenever } from '@vueuse/core';
import type { NInput } from 'naive-ui'; import { NInput } from 'naive-ui';
import { computed, h, ref } from 'vue'; import { computed, h, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import SearchBarItem from './SearchBarItem.vue'; 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 }); const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool });
@ -20,6 +20,12 @@ const inputEl = ref<HTMLElement>();
const displayDropDown = ref(true); const displayDropDown = ref(true);
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac')); 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(() => { const options = computed(() => {
if (queryString.value === '') { if (queryString.value === '') {
return tools.map(toolToOption); return tools.map(toolToOption);
@ -28,12 +34,6 @@ const options = computed(() => {
return searchResult.value.map(toolToOption); return searchResult.value.map(toolToOption);
}); });
const { searchResult } = useFuzzySearch({
search: queryString,
data: tools,
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
});
const keys = useMagicKeys({ const keys = useMagicKeys({
passive: false, passive: false,
onEventFired(e) { onEventFired(e) {
@ -83,13 +83,13 @@ function onFocus() {
:options="options" :options="options"
:on-select="(value: string | number) => onSelect(String(value))" :on-select="(value: string | number) => onSelect(String(value))"
:render-label="renderOption" :render-label="renderOption"
:default-value="'aa'" default-value="aa"
:get-show="() => displayDropDown" :get-show="() => displayDropDown"
:on-focus="onFocus" :on-focus="onFocus"
@update:value="() => (displayDropDown = true)" @update:value="() => (displayDropDown = true)"
> >
<template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }"> <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
<n-input <NInput
ref="inputEl" ref="inputEl"
round round
clearable clearable
@ -103,7 +103,7 @@ function onFocus() {
<template #prefix> <template #prefix>
<n-icon :component="SearchRound" /> <n-icon :component="SearchRound" />
</template> </template>
</n-input> </NInput>
</template> </template>
</n-auto-complete> </n-auto-complete>
</div> </div>

View file

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Tool } from '@/tools/tools.types';
import { toRefs } from 'vue'; import { toRefs } from 'vue';
import type { Tool } from '@/tools/tools.types';
const props = defineProps<{ tool: Tool }>(); const props = defineProps<{ tool: Tool }>();
const { tool } = toRefs(props); const { tool } = toRefs(props);
@ -11,8 +11,12 @@ const { tool } = toRefs(props);
<n-icon class="icon" :component="tool.icon" /> <n-icon class="icon" :component="tool.icon" />
<div> <div>
<div class="name">{{ tool.name }}</div> <div class="name">
<div class="description">{{ tool.description }}</div> {{ tool.name }}
</div>
<div class="description">
{{ tool.description }}
</div>
</div> </div>
</div> </div>
</template> </template>

View file

@ -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"> <script setup lang="ts">
import { useClipboard } from '@vueuse/core'; import { useClipboard } from '@vueuse/core';
import { ref, toRefs } from 'vue'; import { ref, toRefs } from 'vue';
@ -27,6 +18,15 @@ function handleClick() {
} }
</script> </script>
<template>
<n-tooltip trigger="hover">
<template #trigger>
<span class="value" @click="handleClick">{{ value }}</span>
</template>
{{ tooltipText }}
</n-tooltip>
</template>
<style scoped lang="less"> <style scoped lang="less">
.value { .value {
cursor: pointer; cursor: pointer;

View file

@ -1,3 +1,49 @@
<script setup lang="ts">
import { Copy } from '@vicons/tabler';
import { useClipboard, useElementSize } from '@vueuse/core';
import hljs from 'highlight.js/lib/core';
import jsonHljs from 'highlight.js/lib/languages/json';
import sqlHljs from 'highlight.js/lib/languages/sql';
import xmlHljs from 'highlight.js/lib/languages/xml';
import yamlHljs from 'highlight.js/lib/languages/yaml';
import { ref, toRefs } from 'vue';
const props = withDefaults(
defineProps<{
value: string
followHeightOf?: HTMLElement | null
language?: string
copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none'
copyMessage?: string
}>(),
{
followHeightOf: null,
language: 'txt',
copyPlacement: 'top-right',
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.value ? useElementSize(followHeightOf) : { height: ref(null) };
const { copy } = useClipboard({ source: value });
const tooltipText = ref(copyMessage.value);
function onCopyClicked() {
copy();
tooltipText.value = 'Copied !';
setTimeout(() => {
tooltipText.value = copyMessage.value;
}, 2000);
}
</script>
<template> <template>
<div style="overflow-x: hidden; width: 100%"> <div style="overflow-x: hidden; width: 100%">
<c-card class="result-card"> <c-card class="result-card">
@ -22,57 +68,13 @@
</n-tooltip> </n-tooltip>
</c-card> </c-card>
<div v-if="copyPlacement === 'outside'" mt-4 flex justify-center> <div v-if="copyPlacement === 'outside'" mt-4 flex justify-center>
<c-button @click="onCopyClicked"> {{ tooltipText }} </c-button> <c-button @click="onCopyClicked">
{{ tooltipText }}
</c-button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts">
import { Copy } from '@vicons/tabler';
import { useClipboard, useElementSize } from '@vueuse/core';
import hljs from 'highlight.js/lib/core';
import jsonHljs from 'highlight.js/lib/languages/json';
import sqlHljs from 'highlight.js/lib/languages/sql';
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;
}>(),
{
followHeightOf: null,
language: 'txt',
copyPlacement: 'top-right',
copyMessage: 'Copy to clipboard',
},
);
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
const { height } = followHeightOf ? useElementSize(followHeightOf) : { height: ref(null) };
const { copy } = useClipboard({ source: value });
const tooltipText = ref(copyMessage.value);
function onCopyClicked() {
copy();
tooltipText.value = 'Copied !';
setTimeout(() => {
tooltipText.value = copyMessage.value;
}, 2000);
}
</script>
<style lang="less" scoped> <style lang="less" scoped>
::v-deep(.n-scrollbar) { ::v-deep(.n-scrollbar) {
padding-bottom: 10px; padding-bottom: 10px;

View file

@ -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> <template>
<router-link :to="tool.path"> <router-link :to="tool.path">
<c-card class="tool-card"> <c-card class="tool-card">
@ -16,7 +30,7 @@
New New
</n-tag> </n-tag>
<favorite-button :tool="tool" /> <FavoriteButton :tool="tool" />
</div> </div>
</div> </div>
<n-h3 class="title"> <n-h3 class="title">
@ -26,27 +40,13 @@
<div class="description"> <div class="description">
<n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px"> <n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px">
{{ tool.description }} {{ tool.description }}
<br />&nbsp; <br>&nbsp;
</n-ellipsis> </n-ellipsis>
</div> </div>
</c-card> </c-card>
</router-link> </router-link>
</template> </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> <style lang="less" scoped>
a { a {
text-decoration: none; text-decoration: none;

View file

@ -11,7 +11,8 @@ function computedRefreshable<T>(getter: () => T, { throttle }: { throttle?: numb
if (throttle) { if (throttle) {
watchThrottled(getter, update, { throttle }); watchThrottled(getter, update, { throttle });
} else { }
else {
watch(getter, update); watch(getter, update);
} }

View file

@ -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'; import { useMessage } from 'naive-ui';
export function useCopy({ source, text = 'Copied to the clipboard' }: { source: MaybeRef<unknown>; text?: string }) { export function useCopy({ source, text = 'Copied to the clipboard' }: { source: MaybeRef<unknown>; text?: string }) {

View file

@ -5,8 +5,8 @@ function getFileExtensionFromBase64({
base64String, base64String,
defaultExtension = 'txt', defaultExtension = 'txt',
}: { }: {
base64String: string; base64String: string
defaultExtension?: string; defaultExtension?: string
}) { }) {
const hasMimeType = base64String.match(/data:(.*?);base64/i); const hasMimeType = base64String.match(/data:(.*?);base64/i);

View file

@ -1,4 +1,4 @@
import { get, type MaybeRef } from '@vueuse/core'; import { type MaybeRef, get } from '@vueuse/core';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { computed } from 'vue'; import { computed } from 'vue';
@ -9,9 +9,9 @@ function useFuzzySearch<Data>({
data, data,
options = {}, options = {},
}: { }: {
search: MaybeRef<string>; search: MaybeRef<string>
data: Data[]; data: Data[]
options?: Fuse.IFuseOptions<Data>; options?: Fuse.IFuseOptions<Data>
}) { }) {
const fuse = new Fuse(data, options); const fuse = new Fuse(data, options);

View file

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { isFalsyOrHasThrown } from './validation'; import { isFalsyOrHasThrown } from './validation';
@ -11,7 +10,7 @@ describe('useValidation', () => {
expect(isFalsyOrHasThrown(() => {})).toBe(true); expect(isFalsyOrHasThrown(() => {})).toBe(true);
expect( expect(
isFalsyOrHasThrown(() => { isFalsyOrHasThrown(() => {
throw new Error(); throw new Error('message');
}), }),
).toBe(true); ).toBe(true);
}); });

View file

@ -1,45 +1,48 @@
import { get, type MaybeRef } from '@vueuse/core'; import { type MaybeRef, get } from '@vueuse/core';
import _ from 'lodash'; import _ from 'lodash';
import { reactive, watch, type Ref } from 'vue'; import { type Ref, reactive, watch } from 'vue';
type ValidatorReturnType = unknown; type ValidatorReturnType = unknown;
export interface UseValidationRule<T> { export interface UseValidationRule<T> {
validator: (value: T) => ValidatorReturnType; validator: (value: T) => ValidatorReturnType
message: string; message: string
} }
export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
try { try {
const returnValue = cb(); const returnValue = cb();
if (_.isNil(returnValue)) return true; if (_.isNil(returnValue)) {
return true;
}
return returnValue === false; return returnValue === false;
} catch (_) { }
catch (_) {
return true; return true;
} }
} }
export type ValidationAttrs = { export interface ValidationAttrs {
feedback: string; feedback: string
validationStatus: string | undefined; validationStatus: string | undefined
}; }
export function useValidation<T>({ export function useValidation<T>({
source, source,
rules, rules,
watch: watchRefs = [], watch: watchRefs = [],
}: { }: {
source: Ref<T>; source: Ref<T>
rules: MaybeRef<UseValidationRule<T>[]>; rules: MaybeRef<UseValidationRule<T>[]>
watch?: Ref<unknown>[]; watch?: Ref<unknown>[]
}) { }) {
const state = reactive<{ const state = reactive<{
message: string; message: string
status: undefined | 'error'; status: undefined | 'error'
isValid: boolean; isValid: boolean
attrs: ValidationAttrs; attrs: ValidationAttrs
}>({ }>({
message: '', message: '',
status: undefined, status: undefined,

View file

@ -2,7 +2,11 @@
import { NIcon, useThemeVars } from 'naive-ui'; import { NIcon, useThemeVars } from 'naive-ui';
import { computed } from 'vue'; import { computed } from 'vue';
import { RouterLink } from 'vue-router'; 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 { toolsByCategory } from '@/tools';
import { useStyleStore } from '@/stores/style.store'; import { useStyleStore } from '@/stores/style.store';
import { config } from '@/config'; import { config } from '@/config';
@ -10,10 +14,6 @@ import type { ToolCategory } from '@/tools/tools.types';
import { useToolStore } from '@/tools/tools.store'; import { useToolStore } from '@/tools/tools.store';
import { useTracker } from '@/modules/tracker/tracker.services'; import { useTracker } from '@/modules/tracker/tracker.services';
import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue'; 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 themeVars = useThemeVars();
const styleStore = useStyleStore(); const styleStore = useStyleStore();
@ -31,23 +31,27 @@ const tools = computed<ToolCategory[]>(() => [
</script> </script>
<template> <template>
<menu-layout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }"> <MenuLayout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }">
<template #sider> <template #sider>
<router-link to="/" class="hero-wrapper"> <RouterLink to="/" class="hero-wrapper">
<hero-gradient class="gradient" /> <HeroGradient class="gradient" />
<div class="text-wrapper"> <div class="text-wrapper">
<div class="title">IT - TOOLS</div> <div class="title">
IT - TOOLS
</div>
<div class="divider" /> <div class="divider" />
<div class="subtitle">Handy tools for developers</div> <div class="subtitle">
Handy tools for developers
</div>
</div> </div>
</router-link> </RouterLink>
<div class="sider-content"> <div class="sider-content">
<div v-if="styleStore.isSmallScreen" flex justify-center> <div v-if="styleStore.isSmallScreen" flex justify-center>
<navbar-buttons /> <NavbarButtons />
</div> </div>
<collapsible-tool-menu :tools-by-category="tools" /> <CollapsibleToolMenu :tools-by-category="tools" />
<div class="footer"> <div class="footer">
<div> <div>
@ -71,7 +75,9 @@ const tools = computed<ToolCategory[]>(() => [
</div> </div>
<div> <div>
© {{ new Date().getFullYear() }} © {{ 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> </div>
</div> </div>
@ -86,21 +92,21 @@ const tools = computed<ToolCategory[]>(() => [
aria-label="Toggle menu" aria-label="Toggle menu"
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed" @click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
> >
<n-icon size="25" :component="Menu2" /> <NIcon size="25" :component="Menu2" />
</c-button> </c-button>
<n-tooltip trigger="hover"> <n-tooltip trigger="hover">
<template #trigger> <template #trigger>
<c-button to="/" circle variant="text" aria-label="Home"> <c-button to="/" circle variant="text" aria-label="Home">
<n-icon size="25" :component="Home2" /> <NIcon size="25" :component="Home2" />
</c-button> </c-button>
</template> </template>
Home Home
</n-tooltip> </n-tooltip>
<search-bar /> <SearchBar />
<navbar-buttons v-if="!styleStore.isSmallScreen" /> <NavbarButtons v-if="!styleStore.isSmallScreen" />
<n-tooltip trigger="hover"> <n-tooltip trigger="hover">
<template #trigger> <template #trigger>
@ -114,7 +120,7 @@ const tools = computed<ToolCategory[]>(() => [
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
> >
Buy me a coffee 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> </c-button>
</template> </template>
Support IT Tools development ! Support IT Tools development !
@ -122,7 +128,7 @@ const tools = computed<ToolCategory[]>(() => [
</div> </div>
<slot /> <slot />
</template> </template>
</menu-layout> </MenuLayout>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>

View file

@ -3,9 +3,9 @@ import { useRoute } from 'vue-router';
import { useHead } from '@vueuse/head'; import { useHead } from '@vueuse/head';
import type { HeadObject } from '@vueuse/head'; import type { HeadObject } from '@vueuse/head';
import { computed } from 'vue'; import { computed } from 'vue';
import BaseLayout from './base.layout.vue';
import FavoriteButton from '@/components/FavoriteButton.vue'; import FavoriteButton from '@/components/FavoriteButton.vue';
import type { Tool } from '@/tools/tools.types'; import type { Tool } from '@/tools/tools.types';
import BaseLayout from './base.layout.vue';
const route = useRoute(); const route = useRoute();
@ -26,7 +26,7 @@ useHead(head);
</script> </script>
<template> <template>
<base-layout> <BaseLayout>
<div class="tool-layout"> <div class="tool-layout">
<div class="tool-header"> <div class="tool-header">
<div flex flex-nowrap items-center justify-between> <div flex flex-nowrap items-center justify-between>
@ -35,7 +35,7 @@ useHead(head);
</n-h1> </n-h1>
<div> <div>
<favorite-button :tool="{name: route.meta.name} as Tool" /> <FavoriteButton :tool="{ name: route.meta.name } as Tool" />
</div> </div>
</div> </div>
@ -50,7 +50,7 @@ useHead(head);
<div class="tool-content"> <div class="tool-content">
<slot /> <slot />
</div> </div>
</base-layout> </BaseLayout>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>

View file

@ -1,19 +1,19 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import { createHead } from '@vueuse/head'; import { createHead } from '@vueuse/head';
// eslint-disable-next-line import/no-unresolved
import { registerSW } from 'virtual:pwa-register'; import { registerSW } from 'virtual:pwa-register';
import { plausible } from './plugins/plausible.plugin'; import { plausible } from './plugins/plausible.plugin';
import 'virtual:uno.css'; import 'virtual:uno.css';
registerSW();
import { naive } from './plugins/naive.plugin'; import { naive } from './plugins/naive.plugin';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
registerSW();
const app = createApp(App); const app = createApp(App);
app.use(createPinia()); app.use(createPinia());

View file

@ -16,7 +16,7 @@ function useTracker() {
const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible'); const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible');
if (_.isNil(plausible)) { if (_.isNil(plausible)) {
throw new Error('Plausible must be instantiated'); throw new TypeError('Plausible must be instantiated');
} }
const tracker = createTrackerService({ plausible }); const tracker = createTrackerService({ plausible });

View file

@ -9,10 +9,18 @@ useHead({ title: 'Page not found - IT Tools' });
<div mt-20 flex flex-col items-center> <div mt-20 flex flex-col items-center>
<n-icon :component="Coffee" size="100" depth="3" /> <n-icon :component="Coffee" size="100" depth="3" />
<n-h1 m-0 mt-3>404 Not Found</n-h1> <n-h1 m-0 mt-3>
<n-text mt-4 block depth="3">Sorry, this page does not seem to exist</n-text> 404 Not Found
<n-text mb-8 block depth="3">Maybe the cache is doing tricky things, try force-refreshing?</n-text> </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> </div>
</template> </template>

View file

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTracker } from '@/modules/tracker/tracker.services';
import { useHead } from '@vueuse/head'; import { useHead } from '@vueuse/head';
import { useTracker } from '@/modules/tracker/tracker.services';
useHead({ title: 'About - IT Tools' }); useHead({ title: 'About - IT Tools' });
const { tracker } = useTracker(); const { tracker } = useTracker();
@ -11,7 +11,9 @@ const { tracker } = useTracker();
<n-h1>About</n-h1> <n-h1>About</n-h1>
<n-p> <n-p>
This wonderful website, made with by 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 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 ! it to people you think may find it useful too and don't forget to pin it in your shortcut bar !
</n-p> </n-p>
@ -25,8 +27,8 @@ const { tracker } = useTracker();
target="_blank" target="_blank"
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
> >
sponsoring me </c-link sponsoring me
>. </c-link>.
</n-p> </n-p>
<n-h2>Technologies</n-h2> <n-h2>Technologies</n-h2>

View file

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { config } from '@/config';
import { useToolStore } from '@/tools/tools.store';
import { Heart } from '@vicons/tabler'; import { Heart } from '@vicons/tabler';
import { useHead } from '@vueuse/head'; import { useHead } from '@vueuse/head';
import ColoredCard from '../components/ColoredCard.vue'; import ColoredCard from '../components/ColoredCard.vue';
import ToolCard from '../components/ToolCard.vue'; import ToolCard from '../components/ToolCard.vue';
import { useToolStore } from '@/tools/tools.store';
import { config } from '@/config';
const toolStore = useToolStore(); const toolStore = useToolStore();
@ -16,25 +16,23 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
<div class="grid-wrapper"> <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-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
<n-gi> <n-gi>
<colored-card title="You like it-tools?" :icon="Heart"> <ColoredCard title="You like it-tools?" :icon="Heart">
Give us a star on Give us a star on
<a <a
href="https://github.com/CorentinTh/it-tools" href="https://github.com/CorentinTh/it-tools"
rel="noopener" rel="noopener"
target="_blank" target="_blank"
aria-label="IT-Tools' GitHub repository" aria-label="IT-Tools' GitHub repository"
>GitHub</a >GitHub</a>
>
or follow us on or follow us on
<a <a
href="https://twitter.com/ittoolsdottech" href="https://twitter.com/ittoolsdottech"
rel="noopener" rel="noopener"
target="_blank" target="_blank"
aria-label="IT-Tools' Twitter account" aria-label="IT-Tools' Twitter account"
>Twitter</a >Twitter</a>! Thank you
>! Thank you
<n-icon :component="Heart" /> <n-icon :component="Heart" />
</colored-card> </ColoredCard>
</n-gi> </n-gi>
</n-grid> </n-grid>
@ -43,7 +41,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
<n-h3>Your favorite tools</n-h3> <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-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"> <n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name">
<tool-card :tool="tool" /> <ToolCard :tool="tool" />
</n-gi> </n-gi>
</n-grid> </n-grid>
</div> </div>
@ -53,7 +51,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
<n-h3>Newest tools</n-h3> <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-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"> <n-gi v-for="tool in toolStore.newTools" :key="tool.name">
<tool-card :tool="tool" /> <ToolCard :tool="tool" />
</n-gi> </n-gi>
</n-grid> </n-grid>
</div> </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-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"> <n-gi v-for="tool in toolStore.tools" :key="tool.name">
<transition> <transition>
<tool-card :tool="tool" /> <ToolCard :tool="tool" />
</transition> </transition>
</n-gi> </n-gi>
</n-grid> </n-grid>

View file

@ -1,8 +1,8 @@
import { config } from '@/config';
import { noop } from 'lodash'; import { noop } from 'lodash';
import Plausible from 'plausible-tracker'; import Plausible from 'plausible-tracker';
import type { App } from 'vue'; import type { App } from 'vue';
import { config } from '@/config';
function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> { function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> {
return { return {
@ -15,11 +15,11 @@ function createPlausibleInstance({
config, config,
}: { }: {
config: { config: {
isTrackerEnabled: boolean; isTrackerEnabled: boolean
domain: string; domain: string
apiHost: string; apiHost: string
trackLocalhost: boolean; trackLocalhost: boolean
}; }
}) { }) {
if (config.isTrackerEnabled) { if (config.isTrackerEnabled) {
return Plausible(config); return Plausible(config);

View file

@ -15,7 +15,7 @@ const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
const toolsRedirectRoutes = tools const toolsRedirectRoutes = tools
.filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0) .filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0)
.flatMap( .flatMap(
({ path, redirectFrom }) => redirectFrom?.map((redirectSource) => ({ path: redirectSource, redirect: path })) ?? [], ({ path, redirectFrom }) => redirectFrom?.map(redirectSource => ({ path: redirectSource, redirect: path })) ?? [],
); );
const router = createRouter({ const router = createRouter({

View file

@ -1,6 +1,6 @@
import { useMediaQuery, useStorage } from '@vueuse/core'; import { useMediaQuery, useStorage } from '@vueuse/core';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { watch, type Ref } from 'vue'; import { type Ref, watch } from 'vue';
export const useStyleStore = defineStore('style', { export const useStyleStore = defineStore('style', {
state: () => { state: () => {
@ -8,7 +8,7 @@ export const useStyleStore = defineStore('style', {
const isSmallScreen = useMediaQuery('(max-width: 700px)'); const isSmallScreen = useMediaQuery('(max-width: 700px)');
const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>; const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>;
watch(isSmallScreen, (v) => (isMenuCollapsed.value = v)); watch(isSmallScreen, v => (isMenuCollapsed.value = v));
return { return {
isDarkTheme, isDarkTheme,

View file

@ -1,3 +1,51 @@
<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';
const base64Input = ref('');
const { download } = useDownloadFileFromBase64({ source: base64Input });
const base64InputValidation = useValidation({
source: base64Input,
rules: [
{
message: 'Invalid base 64 string',
validator: value => isValidBase64(value.trim()),
},
],
});
function downloadFile() {
if (!base64InputValidation.isValid) {
return;
}
try {
download();
}
catch (_) {
//
}
}
const fileList = ref();
const fileInput = ref() as Ref<File>;
const { base64: fileBase64 } = useBase64(fileInput);
const { copy: copyFileBase64 } = useCopy({ source: fileBase64, text: 'Base64 string copied to the clipboard' });
async function onUpload({ file: { file } }: { file: UploadFileInfo }) {
if (file) {
fileList.value = [];
fileInput.value = file;
}
}
</script>
<template> <template>
<c-card title="Base64 to file"> <c-card title="Base64 to file">
<c-input-text <c-input-text
@ -22,63 +70,22 @@
<div mb-2> <div mb-2>
<n-icon size="35" :depth="3" :component="Upload" /> <n-icon size="35" :depth="3" :component="Upload" />
</div> </div>
<n-text style="font-size: 14px"> Click or drag a file to this area to upload </n-text> <n-text style="font-size: 14px">
Click or drag a file to this area to upload
</n-text>
</n-upload-dragger> </n-upload-dragger>
</n-upload> </n-upload>
<c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 /> <c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 />
<div flex justify-center> <div flex justify-center>
<c-button @click="copyFileBase64()"> Copy </c-button> <c-button @click="copyFileBase64()">
Copy
</c-button>
</div> </div>
</c-card> </c-card>
</template> </template>
<script setup lang="ts">
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 });
const base64InputValidation = useValidation({
source: base64Input,
rules: [
{
message: 'Invalid base 64 string',
validator: (value) => isValidBase64(value.trim()),
},
],
});
function downloadFile() {
if (!base64InputValidation.isValid) return;
try {
download();
} catch (_) {
//
}
}
const fileList = ref();
const fileInput = ref() as Ref<File>;
const { base64: fileBase64 } = useBase64(fileInput);
const { copy: copyFileBase64 } = useCopy({ source: fileBase64, text: 'Base64 string copied to the clipboard' });
async function onUpload({ file: { file } }: { file: UploadFileInfo }) {
if (file) {
fileList.value = [];
fileInput.value = file;
}
}
</script>
<style lang="less" scoped> <style lang="less" scoped>
::v-deep(.n-upload-trigger) { ::v-deep(.n-upload-trigger) {
width: 100%; width: 100%;

View file

@ -4,7 +4,7 @@ import { defineTool } from '../tool';
export const tool = defineTool({ export const tool = defineTool({
name: 'Base64 file converter', name: 'Base64 file converter',
path: '/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'], keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'],
component: () => import('./base64-file-converter.vue'), component: () => import('./base64-file-converter.vue'),
icon: FileDigit, icon: FileDigit,

View file

@ -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> <template>
<c-card title="String to base64"> <c-card title="String to base64">
<n-form-item label="Encode URL safe" label-placement="left"> <n-form-item label="Encode URL safe" label-placement="left">
@ -24,7 +51,9 @@
/> />
<div flex justify-center> <div flex justify-center>
<c-button @click="copyTextBase64()"> Copy base64 </c-button> <c-button @click="copyTextBase64()">
Copy base64
</c-button>
</div> </div>
</c-card> </c-card>
@ -54,34 +83,9 @@
/> />
<div flex justify-center> <div flex justify-center>
<c-button @click="copyText()"> Copy decoded string </c-button> <c-button @click="copyText()">
Copy decoded string
</c-button>
</div> </div>
</c-card> </c-card>
</template> </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>

View file

@ -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> <template>
<div> <div>
<c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 /> <c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 />
@ -19,23 +31,13 @@
</n-statistic> </n-statistic>
</c-card> </c-card>
<div mt-5 flex justify-center> <div mt-5 flex justify-center>
<c-button @click="copy">Copy header</c-button> <c-button @click="copy">
Copy header
</c-button>
</div> </div>
</div> </div>
</template> </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> <style lang="less" scoped>
::v-deep(.n-statistic-value__content) { ::v-deep(.n-statistic-value__content) {
font-family: monospace; font-family: monospace;

View file

@ -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> <template>
<c-card title="Hash"> <c-card title="Hash">
<c-input-text <c-input-text
@ -16,7 +34,9 @@
<c-input-text :value="hashed" readonly text-center /> <c-input-text :value="hashed" readonly text-center />
<div mt-5 flex justify-center> <div mt-5 flex justify-center>
<c-button @click="copy"> Copy hash </c-button> <c-button @click="copy">
Copy hash
</c-button>
</div> </div>
</c-card> </c-card>
@ -37,24 +57,6 @@
</c-card> </c-card>
</template> </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> <style lang="less" scoped>
.compare-result { .compare-result {
color: v-bind('themeVars.errorColor'); color: v-bind('themeVars.errorColor');

View file

@ -13,7 +13,7 @@ function computeAverage({ data }: { data: number[] }) {
function computeVariance({ data }: { data: number[] }) { function computeVariance({ data }: { data: number[] }) {
const mean = computeAverage({ data }); 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 }); return computeAverage({ data: squaredDiffs });
} }
@ -24,11 +24,11 @@ function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; heade
} }
const headers = Object.keys(data[0]); 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 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}`; return `${headerRow}\n${separatorRow}\n${dataRows}`;
} }

View file

@ -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"> <script setup lang="ts">
import { Trash, Plus } from '@vicons/tabler'; import { Plus, Trash } from '@vicons/tabler';
import { useClipboard, useStorage } from '@vueuse/core'; import { useClipboard, useStorage } from '@vueuse/core';
import _ from 'lodash'; import _ from 'lodash';
import { computed } from 'vue'; 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'; import DynamicValues from './dynamic-values.vue';
const suites = useStorage('benchmark-builder:suites', [ const suites = useStorage('benchmark-builder:suites', [
@ -114,8 +34,8 @@ const results = computed(() => {
const deltaWithBestMean = mean - bestMean; const deltaWithBestMean = mean - bestMean;
const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean); const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean);
const comparisonValues: string = const comparisonValues: string
index !== 0 && bestMean !== mean ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : ''; = (index !== 0 && bestMean !== mean) ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
return { return {
position: index + 1, position: index + 1,
@ -157,4 +77,87 @@ function copyAsBulletList() {
} }
</script> </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>

View file

@ -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> <template>
<div> <div>
<div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2> <div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2>
<n-input-number <NInputNumber
:ref="refs.set" :ref="refs.set"
v-model:value="values[index]" v-model:value="values[index]"
:show-button="false" :show-button="false"
@ -25,33 +55,3 @@
</c-button> </c-button>
</div> </div>
</template> </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>

View file

@ -1,3 +1,85 @@
<script setup lang="ts">
import {
chineseSimplifiedWordList,
chineseTraditionalWordList,
czechWordList,
englishWordList,
entropyToMnemonic,
frenchWordList,
generateEntropy,
italianWordList,
japaneseWordList,
koreanWordList,
mnemonicToEntropy,
portugueseWordList,
spanishWordList,
} 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,
'Chinese simplified': chineseSimplifiedWordList,
'Chinese traditional': chineseTraditionalWordList,
'Czech': czechWordList,
'French': frenchWordList,
'Italian': italianWordList,
'Japanese': japaneseWordList,
'Korean': koreanWordList,
'Portuguese': portugueseWordList,
'Spanish': spanishWordList,
};
const entropy = ref(generateEntropy());
const passphraseInput = ref('');
const language = ref<keyof typeof languages>('English');
const passphrase = computed({
get() {
return withDefaultOnError(() => entropyToMnemonic(entropy.value, languages[language.value]), passphraseInput.value);
},
set(value: string) {
passphraseInput.value = value;
entropy.value = withDefaultOnError(() => mnemonicToEntropy(value, languages[language.value]), '');
},
});
const entropyValidation = useValidation({
source: entropy,
rules: [
{
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),
message: 'Entropy should be an hexadecimal string',
},
],
});
const mnemonicValidation = useValidation({
source: passphrase,
rules: [
{
validator: value => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
message: 'Invalid mnemonic',
},
],
});
function refreshEntropy() {
entropy.value = generateEntropy();
}
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> <template>
<div> <div>
<n-grid cols="3" x-gap="12"> <n-grid cols="3" x-gap="12">
@ -47,85 +129,3 @@
</n-form-item> </n-form-item>
</div> </div>
</template> </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,
czechWordList,
englishWordList,
entropyToMnemonic,
frenchWordList,
generateEntropy,
italianWordList,
japaneseWordList,
koreanWordList,
mnemonicToEntropy,
portugueseWordList,
spanishWordList,
} from '@it-tools/bip39';
import { Copy, Refresh } from '@vicons/tabler';
import { computed, ref } from 'vue';
const languages = {
English: englishWordList,
'Chinese simplified': chineseSimplifiedWordList,
'Chinese traditional': chineseTraditionalWordList,
Czech: czechWordList,
French: frenchWordList,
Italian: italianWordList,
Japanese: japaneseWordList,
Korean: koreanWordList,
Portuguese: portugueseWordList,
Spanish: spanishWordList,
};
const entropy = ref(generateEntropy());
const passphraseInput = ref('');
const language = ref<keyof typeof languages>('English');
const passphrase = computed({
get() {
return withDefaultOnError(() => entropyToMnemonic(entropy.value, languages[language.value]), passphraseInput.value);
},
set(value: string) {
passphraseInput.value = value;
entropy.value = withDefaultOnError(() => mnemonicToEntropy(value, languages[language.value]), '');
},
});
const entropyValidation = useValidation({
source: entropy,
rules: [
{
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),
message: 'Entropy should be an hexadecimal string',
},
],
});
const mnemonicValidation = useValidation({
source: passphrase,
rules: [
{
validator: (value) => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
message: 'Invalid mnemonic',
},
],
});
function refreshEntropy() {
entropy.value = generateEntropy();
}
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>

View file

@ -1,111 +1,9 @@
<template>
<div>
<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
<c-alert v-if="permissionCannotBePrompted" mt-4 text-left>
Your browser has blocked permission request or does not support it. You need to grant permission manually in
your browser settings (usually the lock icon in the address bar).
</c-alert>
<div v-else mt-4 flex justify-center>
<c-button @click="requestPermissions">Grant permission</c-button>
</div>
</c-card>
<c-card v-else>
<div flex gap-2>
<div flex-1>
<div>Video</div>
<n-select
v-model:value="currentCamera"
:options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))"
placeholder="Select camera"
/>
</div>
<div v-if="currentMicrophone && microphones.length > 0" flex-1>
<div>Audio</div>
<n-select
v-model:value="currentMicrophone"
:options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))"
placeholder="Select microphone"
/>
</div>
</div>
<div v-if="!isMediaStreamAvailable" mt-3 flex justify-center>
<c-button type="primary" @click="start">Start webcam</c-button>
</div>
<div v-else>
<div my-2>
<video ref="video" autoplay controls playsinline max-h-full w-full />
</div>
<div flex items-center justify-between gap-2>
<c-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot">
<span mr-2> <icon-mdi-camera /></span>
Take screenshot
</c-button>
<div v-if="isRecordingSupported" flex justify-center gap-2>
<c-button v-if="recordingState === 'stopped'" @click="startRecording">
<span mr-2> <icon-mdi-video /></span>
Start recording
</c-button>
<c-button v-if="recordingState === 'recording'" @click="pauseRecording">
<span mr-2> <icon-mdi-pause /></span>
Pause
</c-button>
<c-button v-if="recordingState === 'paused'" @click="resumeRecording">
<span mr-2> <icon-mdi-play /></span>
Resume
</c-button>
<c-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording">
<span mr-2> <icon-mdi-record /></span>
Stop
</c-button>
</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" />
<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 flex gap-2>
<c-button @click="downloadMedia({ type, value, createdAt })">
<icon-mdi-download />
</c-button>
<c-button @click="medias = medias.filter((_ignored, i) => i !== index)">
<icon-mdi-delete-outline />
</c-button>
</div>
</div>
</c-card>
</div>
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import _ from 'lodash'; import _ from 'lodash';
import { useMediaRecorder } from './useMediaRecorder'; import { useMediaRecorder } from './useMediaRecorder';
type Media = { type: 'image' | 'video'; value: string; createdAt: Date }; interface Media { type: 'image' | 'video'; value: string; createdAt: Date }
const { const {
videoInputs: cameras, videoInputs: cameras,
@ -156,19 +54,19 @@ onRecordAvailable((value) => {
}); });
function refreshCurrentDevices() { function refreshCurrentDevices() {
console.log('refreshCurrentDevices'); if (_.isNil(currentCamera) || !cameras.value.find(i => i.deviceId === currentCamera.value)) {
if (_.isNil(currentCamera) || !cameras.value.find((i) => i.deviceId === currentCamera.value)) {
currentCamera.value = cameras.value[0]?.deviceId; currentCamera.value = cameras.value[0]?.deviceId;
} }
if (_.isNil(microphones) || !microphones.value.find((i) => i.deviceId === currentMicrophone.value)) { if (_.isNil(microphones) || !microphones.value.find(i => i.deviceId === currentMicrophone.value)) {
currentMicrophone.value = microphones.value[0]?.deviceId; currentMicrophone.value = microphones.value[0]?.deviceId;
} }
} }
function takeScreenshot() { function takeScreenshot() {
if (!video.value) return; if (!video.value) {
return;
}
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = video.value.videoWidth; canvas.width = video.value.videoWidth;
@ -180,13 +78,16 @@ function takeScreenshot() {
} }
watchEffect(() => { watchEffect(() => {
if (video.value && stream.value) video.value.srcObject = stream.value; if (video.value && stream.value) {
video.value.srcObject = stream.value;
}
}); });
async function requestPermissions() { async function requestPermissions() {
try { try {
await ensurePermissions(); await ensurePermissions();
} catch (e) { }
catch (e) {
permissionCannotBePrompted.value = true; permissionCannotBePrompted.value = true;
} }
} }
@ -199,4 +100,114 @@ function downloadMedia({ type, value, createdAt }: Media) {
} }
</script> </script>
<style lang="less" scoped></style> <template>
<div>
<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
<c-alert v-if="permissionCannotBePrompted" mt-4 text-left>
Your browser has blocked permission request or does not support it. You need to grant permission manually in
your browser settings (usually the lock icon in the address bar).
</c-alert>
<div v-else mt-4 flex justify-center>
<c-button @click="requestPermissions">
Grant permission
</c-button>
</div>
</c-card>
<c-card v-else>
<div flex gap-2>
<div flex-1>
<div>Video</div>
<n-select
v-model:value="currentCamera"
:options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))"
placeholder="Select camera"
/>
</div>
<div v-if="currentMicrophone && microphones.length > 0" flex-1>
<div>Audio</div>
<n-select
v-model:value="currentMicrophone"
:options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))"
placeholder="Select microphone"
/>
</div>
</div>
<div v-if="!isMediaStreamAvailable" mt-3 flex justify-center>
<c-button type="primary" @click="start">
Start webcam
</c-button>
</div>
<div v-else>
<div my-2>
<video ref="video" autoplay controls playsinline max-h-full w-full />
</div>
<div flex items-center justify-between gap-2>
<c-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot">
<span mr-2> <icon-mdi-camera /></span>
Take screenshot
</c-button>
<div v-if="isRecordingSupported" flex justify-center gap-2>
<c-button v-if="recordingState === 'stopped'" @click="startRecording">
<span mr-2> <icon-mdi-video /></span>
Start recording
</c-button>
<c-button v-if="recordingState === 'recording'" @click="pauseRecording">
<span mr-2> <icon-mdi-pause /></span>
Pause
</c-button>
<c-button v-if="recordingState === 'paused'" @click="resumeRecording">
<span mr-2> <icon-mdi-play /></span>
Resume
</c-button>
<c-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording">
<span mr-2> <icon-mdi-record /></span>
Stop
</c-button>
</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">
<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 flex gap-2>
<c-button @click="downloadMedia({ type, value, createdAt })">
<icon-mdi-download />
</c-button>
<c-button @click="medias = medias.filter((_ignored, i) => i !== index)">
<icon-mdi-delete-outline />
</c-button>
</div>
</div>
</c-card>
</div>
</div>
</template>

View file

@ -1,15 +1,15 @@
import { computed, ref, type Ref } from 'vue'; import { type Ref, computed, ref } from 'vue';
export { useMediaRecorder }; export { useMediaRecorder };
function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): { function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): {
isRecordingSupported: Ref<boolean>; isRecordingSupported: Ref<boolean>
recordingState: Ref<'stopped' | 'recording' | 'paused'>; recordingState: Ref<'stopped' | 'recording' | 'paused'>
startRecording: () => void; startRecording: () => void
stopRecording: () => void; stopRecording: () => void
pauseRecording: () => void; pauseRecording: () => void
resumeRecording: () => void; resumeRecording: () => void
onRecordAvailable: (cb: (url: string) => void) => void; onRecordAvailable: (cb: (url: string) => void) => void
} { } {
const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm')); const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm'));
const mediaRecorder = ref<MediaRecorder | null>(null); const mediaRecorder = ref<MediaRecorder | null>(null);
@ -17,10 +17,23 @@ function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }):
const recordAvailable = createEventHook(); const recordAvailable = createEventHook();
const recordingState = ref<'stopped' | 'recording' | 'paused'>('stopped'); 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 = () => { const startRecording = () => {
if (!isRecordingSupported.value) return; if (!isRecordingSupported.value) {
if (!stream.value) return; return;
if (recordingState.value !== 'stopped') return; }
if (!stream.value) {
return;
}
if (recordingState.value !== 'stopped') {
return;
}
mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' }); mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' });
@ -34,47 +47,60 @@ function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }):
recordAvailable.trigger(createVideo()); recordAvailable.trigger(createVideo());
}; };
if (mediaRecorder.value.state !== 'inactive') return; if (mediaRecorder.value.state !== 'inactive') {
return;
}
mediaRecorder.value.start(); mediaRecorder.value.start();
recordingState.value = 'recording'; recordingState.value = 'recording';
}; };
const stopRecording = () => { const stopRecording = () => {
if (!isRecordingSupported.value) return; if (!isRecordingSupported.value) {
if (!mediaRecorder.value) return; return;
if (recordingState.value === 'stopped') return; }
if (!mediaRecorder.value) {
return;
}
if (recordingState.value === 'stopped') {
return;
}
mediaRecorder.value.stop(); mediaRecorder.value.stop();
recordingState.value = 'stopped'; recordingState.value = 'stopped';
}; };
const pauseRecording = () => { const pauseRecording = () => {
if (!isRecordingSupported.value) return; if (!isRecordingSupported.value) {
if (!mediaRecorder.value) return; return;
if (recordingState.value !== 'recording') return; }
if (!mediaRecorder.value) {
return;
}
if (recordingState.value !== 'recording') {
return;
}
mediaRecorder.value.pause(); mediaRecorder.value.pause();
recordingState.value = 'paused'; recordingState.value = 'paused';
}; };
const resumeRecording = () => { const resumeRecording = () => {
if (!isRecordingSupported.value) return; if (!isRecordingSupported.value) {
if (!mediaRecorder.value) return; return;
}
if (!mediaRecorder.value) {
return;
}
if (recordingState.value !== 'paused') return; if (recordingState.value !== 'paused') {
return;
}
mediaRecorder.value.resume(); mediaRecorder.value.resume();
recordingState.value = 'recording'; recordingState.value = 'recording';
}; };
const createVideo = () => {
const blob = new Blob(recordedChunks.value, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
recordedChunks.value = [];
return url;
};
return { return {
isRecordingSupported, isRecordingSupported,
startRecording, startRecording,

View file

@ -1,55 +1,3 @@
<template>
<c-card>
<n-form label-width="120" label-placement="left" :show-feedback="false">
<c-input-text
v-model:value="input"
label="Your string"
label-position="left"
label-width="120px"
label-align="right"
placeholder="Your string..."
raw-text
/>
<n-divider />
<n-form-item label="Camelcase:">
<input-copyable :value="camelCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Capitalcase:">
<input-copyable :value="capitalCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Constantcase:">
<input-copyable :value="constantCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Dotcase:">
<input-copyable :value="dotCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Headercase:">
<input-copyable :value="headerCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Nocase:">
<input-copyable :value="noCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Paramcase:">
<input-copyable :value="paramCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Pascalcase:">
<input-copyable :value="pascalCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Pathcase:">
<input-copyable :value="pathCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Sentencecase:">
<input-copyable :value="sentenceCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Snakecase:">
<input-copyable :value="snakeCase(input, baseConfig)" />
</n-form-item>
</n-form>
</c-card>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { import {
@ -74,6 +22,58 @@ const baseConfig = {
const input = ref('lorem ipsum dolor sit amet'); const input = ref('lorem ipsum dolor sit amet');
</script> </script>
<template>
<c-card>
<n-form label-width="120" label-placement="left" :show-feedback="false">
<c-input-text
v-model:value="input"
label="Your string"
label-position="left"
label-width="120px"
label-align="right"
placeholder="Your string..."
raw-text
/>
<n-divider />
<n-form-item label="Camelcase:">
<InputCopyable :value="camelCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Capitalcase:">
<InputCopyable :value="capitalCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Constantcase:">
<InputCopyable :value="constantCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Dotcase:">
<InputCopyable :value="dotCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Headercase:">
<InputCopyable :value="headerCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Nocase:">
<InputCopyable :value="noCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Paramcase:">
<InputCopyable :value="paramCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Pascalcase:">
<InputCopyable :value="pascalCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Pathcase:">
<InputCopyable :value="pathCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Sentencecase:">
<InputCopyable :value="sentenceCase(input, baseConfig)" />
</n-form-item>
<n-form-item label="Snakecase:">
<InputCopyable :value="snakeCase(input, baseConfig)" />
</n-form-item>
</n-form>
</c-card>
</template>
<style lang="less" scoped> <style lang="less" scoped>
.n-form-item { .n-form-item {
margin: 5px 0; margin: 5px 0;

View file

@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { computeChmodOctalRepresentation } from './chmod-calculator.service'; import { computeChmodOctalRepresentation } from './chmod-calculator.service';
describe('chmod-calculator', () => { describe('chmod-calculator', () => {

View file

@ -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"> <script setup lang="ts">
import { useThemeVars } from 'naive-ui'; import { useThemeVars } from 'naive-ui';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
import InputCopyable from '../../components/InputCopyable.vue'; import InputCopyable from '../../components/InputCopyable.vue';
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
import type { Group, Scope } from './chmod-calculator.types'; import type { Group, Scope } from './chmod-calculator.types';
@ -54,6 +24,44 @@ const permissions = ref({
const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value })); const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
</script> </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> <style lang="less" scoped>
.octal-result { .octal-result {
text-align: center; text-align: center;

View file

@ -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"> <script setup lang="ts">
import { useRafFn } from '@vueuse/core'; import { useRafFn } from '@vueuse/core';
import { ref } from 'vue'; import { ref } from 'vue';
@ -42,6 +28,28 @@ function pause() {
} }
</script> </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> <style lang="less" scoped>
.duration { .duration {
text-align: center; text-align: center;

View file

@ -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"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { colord, extend } from 'colord'; import { colord, extend } from 'colord';
@ -57,17 +22,67 @@ function onInputUpdated(value: string, omit: string) {
try { try {
const color = colord(value); const color = colord(value);
if (omit !== 'name') name.value = color.toName({ closest: true }) ?? ''; if (omit !== 'name') {
if (omit !== 'hex') hex.value = color.toHex(); name.value = color.toName({ closest: true }) ?? '';
if (omit !== 'rgb') rgb.value = color.toRgbString(); }
if (omit !== 'hsl') hsl.value = color.toHslString(); if (omit !== 'hex') {
if (omit !== 'hwb') hwb.value = color.toHwbString(); hex.value = color.toHex();
if (omit !== 'cmyk') cmyk.value = color.toCmykString(); }
if (omit !== 'lch') lch.value = color.toLchString(); if (omit !== 'rgb') {
} catch { 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'); onInputUpdated(hex.value, 'hex');
</script> </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>

View file

@ -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"> <script setup lang="ts">
import cronstrue from 'cronstrue'; import cronstrue from 'cronstrue';
import { isValidCron } from 'cron-validator'; import { isValidCron } from 'cron-validator';
@ -194,6 +108,97 @@ const cronValidationRules = [
]; ];
</script> </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> <style lang="less" scoped>
::v-deep(input) { ::v-deep(input) {
font-size: 30px; font-size: 30px;

View file

@ -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.describe('Date time converter - json to yaml', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {

View file

@ -1,13 +1,13 @@
import { describe, test, expect } from 'vitest'; import { describe, expect, test } from 'vitest';
import { import {
isISO8601DateTimeString, isISO8601DateTimeString,
isISO9075DateString, isISO9075DateString,
isMongoObjectId,
isRFC3339DateString, isRFC3339DateString,
isRFC7231DateString, isRFC7231DateString,
isUnixTimestamp,
isTimestamp, isTimestamp,
isUTCDateString, isUTCDateString,
isMongoObjectId, isUnixTimestamp,
} from './date-time-converter.models'; } from './date-time-converter.models';
describe('date-time-converter models', () => { describe('date-time-converter models', () => {

View file

@ -11,13 +11,13 @@ export {
isMongoObjectId, isMongoObjectId,
}; };
const ISO8601_REGEX = 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)?)?)?)?$/; = /^([+-]?\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 = 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)?$/; = /^([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 = 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)$/; = /^([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$/; 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 { try {
return new Date(date).toUTCString() === date; return new Date(date).toUTCString() === date;
} catch (_ignored) { }
catch (_ignored) {
return false; return false;
} }
} }

View file

@ -1,8 +1,8 @@
export type ToDateMapper = (value: string) => Date; export type ToDateMapper = (value: string) => Date;
export type DateFormat = { export interface DateFormat {
name: string; name: string
fromDate: (date: Date) => string; fromDate: (date: Date) => string
toDate: (value: string) => Date; toDate: (value: string) => Date
formatMatcher: (dateString: string) => boolean; formatMatcher: (dateString: string) => boolean
}; }

View file

@ -1,3 +1,145 @@
<script setup lang="ts">
import {
formatISO,
formatISO9075,
formatRFC3339,
formatRFC7231,
fromUnixTime,
getTime,
getUnixTime,
isDate,
isValid,
parseISO,
parseJSON,
} from 'date-fns';
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
import {
isISO8601DateTimeString,
isISO9075DateString,
isMongoObjectId,
isRFC3339DateString,
isRFC7231DateString,
isTimestamp,
isUTCDateString,
isUnixTimestamp,
} 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);
const formats: DateFormat[] = [
{
name: 'JS locale date string',
fromDate: date => date.toString(),
toDate,
formatMatcher: () => false,
},
{
name: 'ISO 8601',
fromDate: formatISO,
toDate: parseISO,
formatMatcher: date => isISO8601DateTimeString(date),
},
{
name: 'ISO 9075',
fromDate: formatISO9075,
toDate: parseISO,
formatMatcher: date => isISO9075DateString(date),
},
{
name: 'RFC 3339',
fromDate: formatRFC3339,
toDate,
formatMatcher: date => isRFC3339DateString(date),
},
{
name: 'RFC 7231',
fromDate: formatRFC7231,
toDate,
formatMatcher: date => isRFC7231DateString(date),
},
{
name: 'Unix timestamp',
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),
},
{
name: 'UTC format',
fromDate: date => date.toUTCString(),
toDate,
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),
},
];
const formatIndex = ref(6);
const now = useNow();
const normalizedDate = computed(() => {
if (!inputDate.value) {
return now.value;
}
const { toDate } = formats[formatIndex.value];
try {
return toDate(inputDate.value);
}
catch (_ignored) {
return undefined;
}
});
function onDateInputChanged(value: string) {
const matchingIndex = formats.findIndex(({ formatMatcher }) => formatMatcher(value));
if (matchingIndex !== -1) {
formatIndex.value = matchingIndex;
}
}
const validation = useValidation({
source: inputDate,
watch: [formatIndex],
rules: [
{
message: 'This date is invalid for this format',
validator: value =>
withDefaultOnError(() => {
if (value === '') {
return true;
}
const maybeDate = formats[formatIndex.value].toDate(value);
return isDate(maybeDate) && isValid(maybeDate);
}, false),
},
],
});
function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) {
if (!date || !validation.isValid) {
return '';
}
return withDefaultOnError(() => formatter(date), '');
}
</script>
<template> <template>
<div> <div>
<n-input-group> <n-input-group>
@ -36,142 +178,3 @@
/> />
</div> </div>
</template> </template>
<script setup lang="ts">
import {
formatISO,
formatISO9075,
formatRFC3339,
formatRFC7231,
fromUnixTime,
getTime,
getUnixTime,
parseISO,
parseJSON,
isDate,
isValid,
} 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,
isRFC3339DateString,
isRFC7231DateString,
isTimestamp,
isUTCDateString,
isUnixTimestamp,
isMongoObjectId,
} from './date-time-converter.models';
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 formats: DateFormat[] = [
{
name: 'JS locale date string',
fromDate: (date) => date.toString(),
toDate,
formatMatcher: () => false,
},
{
name: 'ISO 8601',
fromDate: formatISO,
toDate: parseISO,
formatMatcher: (date) => isISO8601DateTimeString(date),
},
{
name: 'ISO 9075',
fromDate: formatISO9075,
toDate: parseISO,
formatMatcher: (date) => isISO9075DateString(date),
},
{
name: 'RFC 3339',
fromDate: formatRFC3339,
toDate,
formatMatcher: (date) => isRFC3339DateString(date),
},
{
name: 'RFC 7231',
fromDate: formatRFC7231,
toDate,
formatMatcher: (date) => isRFC7231DateString(date),
},
{
name: 'Unix timestamp',
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),
},
{
name: 'UTC format',
fromDate: (date) => date.toUTCString(),
toDate,
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),
},
];
const formatIndex = ref(6);
const now = useNow();
const normalizedDate = computed(() => {
if (!inputDate.value) {
return now.value;
}
const { toDate } = formats[formatIndex.value];
try {
return toDate(inputDate.value);
} catch (_ignored) {
return undefined;
}
});
function onDateInputChanged(value: string) {
const matchingIndex = formats.findIndex(({ formatMatcher }) => formatMatcher(value));
if (matchingIndex !== -1) {
formatIndex.value = matchingIndex;
}
}
const validation = useValidation({
source: inputDate,
watch: [formatIndex],
rules: [
{
message: 'This date is invalid for this format',
validator: (value) =>
withDefaultOnError(() => {
if (value === '') return true;
const maybeDate = formats[formatIndex.value].toDate(value);
return isDate(maybeDate) && isValid(maybeDate);
}, false),
},
],
});
</script>

View file

@ -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"> <script setup lang="ts">
import { useWindowSize } from '@vueuse/core'; import { useWindowSize } from '@vueuse/core';
import { computed } from 'vue'; import { computed } from 'vue';
@ -77,6 +58,27 @@ const sections = [
]; ];
</script> </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> <style lang="less" scoped>
.information { .information {
padding: 14px 16px; padding: 14px 16px;

View file

@ -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> <template>
<div> <div>
<n-form-item label="Your docker run command:" :show-feedback="false"> <n-form-item label="Your docker run command:" :show-feedback="false">
@ -12,16 +43,20 @@
<n-divider /> <n-divider />
<textarea-copyable :value="dockerCompose" language="yaml" /> <TextareaCopyable :value="dockerCompose" language="yaml" />
<div mt-5 flex justify-center> <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>
<div v-if="notComposable.length > 0"> <div v-if="notComposable.length > 0">
<n-alert title="This options are not translatable to docker-compose" type="info" mt-5> <n-alert title="This options are not translatable to docker-compose" type="info" mt-5>
<ul> <ul>
<li v-for="(message, index) of notComposable" :key="index">{{ message }}</li> <li v-for="(message, index) of notComposable" :key="index">
{{ message }}
</li>
</ul> </ul>
</n-alert> </n-alert>
</div> </div>
@ -33,7 +68,9 @@
mt-5 mt-5
> >
<ul> <ul>
<li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li> <li v-for="(message, index) of notImplemented" :key="index">
{{ message }}
</li>
</ul> </ul>
</n-alert> </n-alert>
</div> </div>
@ -41,41 +78,11 @@
<div v-if="errors.length > 0"> <div v-if="errors.length > 0">
<n-alert title="The following errors occured" type="error" mt-5> <n-alert title="The following errors occured" type="error" mt-5>
<ul> <ul>
<li v-for="(message, index) of errors" :key="index">{{ message }}</li> <li v-for="(message, index) of errors" :key="index">
{{ message }}
</li>
</ul> </ul>
</n-alert> </n-alert>
</div> </div>
</div> </div>
</template> </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>

View file

@ -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> <template>
<c-card title="Encrypt"> <c-card title="Encrypt">
<div flex gap-3> <div flex gap-3>
@ -78,22 +97,3 @@
</n-form-item> </n-form-item>
</c-card> </c-card>
</template> </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>

View file

@ -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> <template>
<div> <div>
<n-text depth="3" style="text-align: justify; width: 100%; display: inline-block"> <n-text depth="3" style="text-align: justify; width: 100%; display: inline-block">
@ -29,45 +54,24 @@
{ label: 'hours', value: 1000 * 60 * 60 }, { label: 'hours', value: 1000 * 60 * 60 },
{ label: 'days', value: 1000 * 60 * 60 * 24 }, { label: 'days', value: 1000 * 60 * 60 * 24 },
]" ]"
></n-select> />
</n-input-group> </n-input-group>
</n-form-item> </n-form-item>
<n-divider /> <n-divider />
<c-card mb-2> <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>
<c-card> <c-card>
<n-statistic label="It will end ">{{ endAt }}</n-statistic> <n-statistic label="It will end ">
{{ endAt }}
</n-statistic>
</c-card> </c-card>
</div> </div>
</template> </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> <style lang="less" scoped>
.n-input-number, .n-input-number,
.n-date-picker { .n-date-picker {

View file

@ -1,9 +1,3 @@
<template>
<div>
<memo />
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { useThemeVars } from 'naive-ui'; import { useThemeVars } from 'naive-ui';
import Memo from './git-memo.md'; import Memo from './git-memo.md';
@ -11,6 +5,12 @@ import Memo from './git-memo.md';
const themeVars = useThemeVars(); const themeVars = useThemeVars();
</script> </script>
<template>
<div>
<Memo />
</div>
</template>
<style lang="less" scoped> <style lang="less" scoped>
::v-deep(pre) { ::v-deep(pre) {
margin: 0; margin: 0;

View file

@ -2,6 +2,6 @@ export function convertHexToBin(hex: string) {
return hex return hex
.trim() .trim()
.split('') .split('')
.map((byte) => parseInt(byte, 16).toString(2).padStart(4, '0')) .map(byte => parseInt(byte, 16).toString(2).padStart(4, '0'))
.join(''); .join('');
} }

View file

@ -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> <template>
<div> <div>
<c-card> <c-card>
@ -31,45 +67,12 @@
<div v-for="algo in algoNames" :key="algo" style="margin: 5px 0"> <div v-for="algo in algoNames" :key="algo" style="margin: 5px 0">
<n-input-group> <n-input-group>
<n-input-group-label style="flex: 0 0 120px"> {{ algo }} </n-input-group-label> <n-input-group-label style="flex: 0 0 120px">
<input-copyable :value="hashText(algo, clearText)" readonly /> {{ algo }}
</n-input-group-label>
<InputCopyable :value="hashText(algo, clearText)" readonly />
</n-input-group> </n-input-group>
</div> </div>
</c-card> </c-card>
</div> </div>
</template> </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>

View file

@ -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> <template>
<div> <div>
<n-form-item label="Plain text to compute the hash"> <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-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." />
</n-form-item> </n-form-item>
<div flex justify-center> <div flex justify-center>
<c-button @click="copy()">Copy HMAC</c-button> <c-button @click="copy()">
Copy HMAC
</c-button>
</div> </div>
</div> </div>
</template> </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>

View file

@ -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('&lt;title&gt;IT Tool&lt;/title');
const unescapeOutput = computed(() => unescape(unescapeInput.value));
const { copy: copyUnescaped } = useCopy({ source: unescapeOutput });
</script>
<template> <template>
<c-card title="Escape html entities"> <c-card title="Escape html entities">
<n-form-item label="Your string :"> <n-form-item label="Your string :">
@ -20,7 +34,9 @@
</n-form-item> </n-form-item>
<div flex justify-center> <div flex justify-center>
<c-button @click="copyEscaped"> Copy </c-button> <c-button @click="copyEscaped">
Copy
</c-button>
</div> </div>
</c-card> </c-card>
<c-card title="Unescape html entities"> <c-card title="Unescape html entities">
@ -44,21 +60,9 @@
</n-form-item> </n-form-item>
<div flex justify-center> <div flex justify-center>
<c-button @click="copyUnescaped"> Copy </c-button> <c-button @click="copyUnescaped">
Copy
</c-button>
</div> </div>
</c-card> </c-card>
</template> </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('&lt;title&gt;IT Tool&lt;/title');
const unescapeOutput = computed(() => unescape(unescapeInput.value));
const { copy: copyUnescaped } = useCopy({ source: unescapeOutput });
</script>

View file

@ -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"> <script setup lang="ts">
import { tryOnBeforeUnmount, useVModel } from '@vueuse/core'; import { tryOnBeforeUnmount, useVModel } from '@vueuse/core';
import { Editor, EditorContent } from '@tiptap/vue-3'; import { Editor, EditorContent } from '@tiptap/vue-3';
@ -16,9 +5,9 @@ import StarterKit from '@tiptap/starter-kit';
import { useThemeVars } from 'naive-ui'; import { useThemeVars } from 'naive-ui';
import MenuBar from './menu-bar.vue'; import MenuBar from './menu-bar.vue';
const themeVars = useThemeVars();
const props = defineProps<{ html: string }>(); const props = defineProps<{ html: string }>();
const emit = defineEmits(['update:html']); const emit = defineEmits(['update:html']);
const themeVars = useThemeVars();
const html = useVModel(props, 'html', emit); const html = useVModel(props, 'html', emit);
const editor = new Editor({ const editor = new Editor({
@ -33,6 +22,17 @@ tryOnBeforeUnmount(() => {
}); });
</script> </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"> <style scoped lang="less">
::v-deep(.ProseMirror-focused) { ::v-deep(.ProseMirror-focused) {
outline: none; outline: none;

View file

@ -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> <template>
<n-tooltip trigger="hover"> <n-tooltip trigger="hover">
<template #trigger> <template #trigger>
@ -9,12 +16,3 @@
{{ title }} {{ title }}
</n-tooltip> </n-tooltip>
</template> </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>

View file

@ -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"> <script setup lang="ts">
import type { Editor } from '@tiptap/vue-3'; import type { Editor } from '@tiptap/vue-3';
import { import {
@ -27,7 +18,7 @@ import {
Strikethrough, Strikethrough,
TextWrap, TextWrap,
} from '@vicons/tabler'; } from '@vicons/tabler';
import { toRefs, type Component } from 'vue'; import { type Component, toRefs } from 'vue';
import MenuBarItem from './menu-bar-item.vue'; import MenuBarItem from './menu-bar-item.vue';
const props = defineProps<{ editor: Editor }>(); const props = defineProps<{ editor: Editor }>();
@ -35,12 +26,12 @@ const { editor } = toRefs(props);
type MenuItem = type MenuItem =
| { | {
icon: Component; icon: Component
title: string; title: string
action: () => void; action: () => void
isActive?: () => boolean; isActive?: () => boolean
type: 'button'; type: 'button'
} }
| { type: 'divider' }; | { type: 'divider' };
const items: MenuItem[] = [ const items: MenuItem[] = [
@ -166,4 +157,11 @@ const items: MenuItem[] = [
]; ];
</script> </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>

View file

@ -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"> <script setup lang="ts">
import TextareaCopyable from '@/components/TextareaCopyable.vue';
import { format } from 'prettier'; import { format } from 'prettier';
import htmlParser from 'prettier/parser-html'; import htmlParser from 'prettier/parser-html';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import Editor from './editor/editor.vue'; 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>'); const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>');
</script> </script>
<style lang="less" scoped></style> <template>
<Editor v-model:html="html" />
<TextareaCopyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" />
</template>

View file

@ -1,11 +1,11 @@
export const codesByCategories: { export const codesByCategories: {
category: string; category: string
codes: { codes: {
code: number; code: number
name: string; name: string
description: string; description: string
type: 'HTTP' | 'WebDav'; type: 'HTTP' | 'WebDav'
}[]; }[]
}[] = [ }[] = [
{ {
category: '1xx informational response', category: '1xx informational response',
@ -286,7 +286,7 @@ export const codesByCategories: {
}, },
{ {
code: 418, code: 418,
name: "I'm a teapot", name: 'I\'m a teapot',
description: 'The server refuses the attempt to brew coffee with a teapot.', description: 'The server refuses the attempt to brew coffee with a teapot.',
type: 'HTTP', type: 'HTTP',
}, },

View file

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test'; import { expect, test } from '@playwright/test';
test.describe('Tool - Http status codes', () => { test.describe('Tool - Http status codes', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {

View file

@ -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> <template>
<div> <div>
<n-form-item :show-label="false"> <n-form-item :show-label="false">
@ -21,35 +45,13 @@
<n-h2> {{ category }} </n-h2> <n-h2> {{ category }} </n-h2>
<c-card v-for="{ code, description, name, type } of codes" :key="code" mb-2> <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 strong block text-lg>
<n-text block depth="3">{{ description }} {{ type !== 'HTTP' ? `For ${type}.` : '' }}</n-text> {{ code }} {{ name }}
</n-text>
<n-text block depth="3">
{{ description }} {{ type !== 'HTTP' ? `For ${type}.` : '' }}
</n-text>
</c-card> </c-card>
</div> </div>
</div> </div>
</template> </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>

View file

@ -140,5 +140,5 @@ export const toolsByCategory: ToolCategory[] = [
export const tools = toolsByCategory.flatMap(({ components }) => components); export const tools = toolsByCategory.flatMap(({ components }) => components);
export const toolsWithCategory = toolsByCategory.flatMap(({ components, name }) => export const toolsWithCategory = toolsByCategory.flatMap(({ components, name }) =>
components.map((tool) => ({ category: name, ...tool })), components.map(tool => ({ category: name, ...tool })),
); );

View file

@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { convertBase } from './integer-base-converter.model'; import { convertBase } from './integer-base-converter.model';
describe('integer-base-converter', () => { describe('integer-base-converter', () => {

View file

@ -7,9 +7,9 @@ export function convertBase({ value, fromBase, toBase }: { value: string; fromBa
.reverse() .reverse()
.reduce((carry: number, digit: string, index: number) => { .reduce((carry: number, digit: string, index: number) => {
if (!fromRange.includes(digit)) { if (!fromRange.includes(digit)) {
throw new Error('Invalid digit "' + digit + '" for base ' + fromBase + '.'); throw new Error(`Invalid digit "${digit}" for base ${fromBase}.`);
} }
return (carry += fromRange.indexOf(digit) * Math.pow(fromBase, index)); return (carry += fromRange.indexOf(digit) * fromBase ** index);
}, 0); }, 0);
let newValue = ''; let newValue = '';
while (decValue > 0) { while (decValue > 0) {

View file

@ -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> <template>
<div> <div>
<c-card> <c-card>
<div v-if="styleStore.isSmallScreen"> <div v-if="styleStore.isSmallScreen">
<n-input-group> <n-input-group>
<n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label> <n-input-group-label style="flex: 0 0 120px">
Input number:
</n-input-group-label>
<n-input v-model:value="input" w-full :status="error ? 'error' : undefined" /> <n-input v-model:value="input" w-full :status="error ? 'error' : undefined" />
</n-input-group> </n-input-group>
<n-input-group> <n-input-group>
<n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label> <n-input-group-label style="flex: 0 0 120px">
Input base:
</n-input-group-label>
<n-input-number v-model:value="inputBase" max="64" min="2" w-full /> <n-input-number v-model:value="inputBase" max="64" min="2" w-full />
</n-input-group> </n-input-group>
</div> </div>
<n-input-group v-else> <n-input-group v-else>
<n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label> <n-input-group-label style="flex: 0 0 120px">
Input number:
</n-input-group-label>
<n-input v-model:value="input" :status="error ? 'error' : undefined" /> <n-input v-model:value="input" :status="error ? 'error' : undefined" />
<n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label> <n-input-group-label style="flex: 0 0 120px">
Input base:
</n-input-group-label>
<n-input-number v-model:value="inputBase" max="64" min="2" /> <n-input-number v-model:value="inputBase" max="64" min="2" />
</n-input-group> </n-input-group>
<n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert> <n-alert v-if="error" style="margin-top: 25px" type="error">
{{ error }}
</n-alert>
<n-divider /> <n-divider />
<input-copyable <InputCopyable
label="Binary (2)" label="Binary (2)"
v-bind="inputProps" v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
placeholder="Binary version will be here..." placeholder="Binary version will be here..."
/> />
<input-copyable <InputCopyable
label="Octal (8)" label="Octal (8)"
v-bind="inputProps" v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
placeholder="Octal version will be here..." placeholder="Octal version will be here..."
/> />
<input-copyable <InputCopyable
label="Decimal (10)" label="Decimal (10)"
v-bind="inputProps" v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
placeholder="Decimal version will be here..." placeholder="Decimal version will be here..."
/> />
<input-copyable <InputCopyable
label="Hexadecimal (16)" label="Hexadecimal (16)"
v-bind="inputProps" v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })"
placeholder="Hexadecimal version will be here..." placeholder="Hexadecimal version will be here..."
/> />
<input-copyable <InputCopyable
label="Base64 (64)" label="Base64 (64)"
v-bind="inputProps" v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })" :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-number v-model:value="outputBase" max="64" min="2" />
</n-input-group> </n-input-group>
<input-copyable <InputCopyable
flex-1 flex-1
v-bind="inputProps" v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })"
@ -74,42 +121,6 @@
</div> </div>
</template> </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> <style lang="less" scoped>
.n-input-group:not(:first-child) { .n-input-group:not(:first-child) {
margin-top: 5px; margin-top: 5px;

View file

@ -1,5 +1,5 @@
import { expect, describe, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { isValidIpv4, ipv4ToInt } from './ipv4-address-converter.service'; import { ipv4ToInt, isValidIpv4 } from './ipv4-address-converter.service';
describe('ipv4-address-converter', () => { describe('ipv4-address-converter', () => {
describe('ipv4ToInt', () => { describe('ipv4ToInt', () => {

View file

@ -10,7 +10,7 @@ function ipv4ToInt({ ip }: { ip: string }) {
return ip return ip
.trim() .trim()
.split('.') .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 }) { 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 ( return (
prefix + prefix
_.chain(ip) + _.chain(ip)
.trim() .trim()
.split('.') .split('.')
.map((part) => parseInt(part).toString(16).padStart(2, '0')) .map(part => parseInt(part).toString(16).padStart(2, '0'))
.chunk(2) .chunk(2)
.map((blocks) => blocks.join('')) .map(blocks => blocks.join(''))
.join(':') .join(':')
.value() .value()
); );

View file

@ -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"> <script setup lang="ts">
import { useValidation } from '@/composable/validation';
import { convertBase } from '../integer-base-converter/integer-base-converter.model'; import { convertBase } from '../integer-base-converter/integer-base-converter.model';
import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from './ipv4-address-converter.service'; import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from './ipv4-address-converter.service';
import { useValidation } from '@/composable/validation';
const rawIpAddress = useStorage('ipv4-converter:ip', '192.168.1.1'); const rawIpAddress = useStorage('ipv4-converter:ip', '192.168.1.1');
@ -54,8 +34,26 @@ const convertedSections = computed(() => {
const { attrs: validationAttrs } = useValidation({ const { attrs: validationAttrs } = useValidation({
source: rawIpAddress, source: rawIpAddress,
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }], rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }],
}); });
</script> </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>

View file

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test'; import { expect, test } from '@playwright/test';
test.describe('Tool - IPv4 range expander', () => { test.describe('Tool - IPv4 range expander', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {

View file

@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { calculateCidr } from './ipv4-range-expander.service'; import { calculateCidr } from './ipv4-range-expander.service';
describe('ipv4RangeExpander', () => { describe('ipv4RangeExpander', () => {

View file

@ -1,20 +1,25 @@
import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types';
import { convertBase } from '../integer-base-converter/integer-base-converter.model'; import { convertBase } from '../integer-base-converter/integer-base-converter.model';
import { ipv4ToInt } from '../ipv4-address-converter/ipv4-address-converter.service'; import { ipv4ToInt } from '../ipv4-address-converter/ipv4-address-converter.service';
import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types';
export { calculateCidr }; export { calculateCidr };
function bits2ip(ipInt: number) { 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) { 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); return 1 + parseInt(end, 2) - parseInt(start, 2);
} }
function getCidr(start: string, end: string) { 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); const range = getRangesize(start, end);
if (range < 1) { if (range < 1) {
@ -32,7 +37,7 @@ function getCidr(start: string, end: string) {
const newStart = start.substring(0, mask) + '0'.repeat(32 - mask); const newStart = start.substring(0, mask) + '0'.repeat(32 - mask);
const newEnd = end.substring(0, mask) + '1'.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 }) { function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) {
@ -52,7 +57,7 @@ function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) {
const result: Ipv4RangeExpanderResult = {}; const result: Ipv4RangeExpanderResult = {};
result.newEnd = bits2ip(parseInt(cidr.end, 2)); result.newEnd = bits2ip(parseInt(cidr.end, 2));
result.newStart = bits2ip(parseInt(cidr.start, 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.newSize = getRangesize(cidr.start, cidr.end);
result.oldSize = getRangesize(start, end); result.oldSize = getRangesize(start, end);

View file

@ -1,7 +1,7 @@
export type Ipv4RangeExpanderResult = { export interface Ipv4RangeExpanderResult {
oldSize?: number; oldSize?: number
newStart?: string; newStart?: string
newEnd?: string; newEnd?: string
newCidr?: string; newCidr?: string
newSize?: number; newSize?: number
}; }

View file

@ -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> <template>
<div> <div>
<div mb-4 flex gap-4> <div mb-4 flex gap-4>
@ -21,13 +79,19 @@
<n-table v-if="showResult" data-test-id="result"> <n-table v-if="showResult" data-test-id="result">
<thead> <thead>
<tr> <tr>
<th scope="col">&nbsp;</th> <th scope="col">
<th scope="col">old value</th> &nbsp;
<th scope="col">new value</th> </th>
<th scope="col">
old value
</th>
<th scope="col">
new value
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<result-row <ResultRow
v-for="{ label, getOldValue, getNewValue } in calculatedValues" v-for="{ label, getOldValue, getNewValue } in calculatedValues"
:key="label" :key="label"
:label="label" :label="label"
@ -53,62 +117,3 @@
</n-alert> </n-alert>
</div> </div>
</template> </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>

View file

@ -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"> <script setup lang="ts">
import SpanCopyable from '@/components/SpanCopyable.vue';
import _ from 'lodash'; import _ from 'lodash';
import SpanCopyable from '@/components/SpanCopyable.vue';
const props = withDefaults(defineProps<{ label: string; oldValue?: string; newValue?: string }>(), { const props = withDefaults(defineProps<{ label: string; oldValue?: string; newValue?: string }>(), {
label: '', label: '',
@ -24,4 +12,18 @@ const { label, oldValue, newValue } = toRefs(props);
const testId = computed(() => _.kebabCase(label.value)); const testId = computed(() => _.kebabCase(label.value));
</script> </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>

View file

@ -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"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { Netmask } from 'netmask'; import { Netmask } from 'netmask';
import { withDefaultOnError } from '@/utils/defaults';
import { isNotThrowing } from '@/utils/boolean';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import { ArrowLeft, ArrowRight } from '@vicons/tabler'; import { ArrowLeft, ArrowRight } from '@vicons/tabler';
import SpanCopyable from '@/components/SpanCopyable.vue';
import { getIPClass } from './ipv4-subnet-calculator.models'; 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'); const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24');
@ -61,13 +22,13 @@ const ipValidationRules = [
]; ];
const sections: { const sections: {
label: string; label: string
getValue: (blocks: Netmask) => string | undefined; getValue: (blocks: Netmask) => string | undefined
undefinedFallback?: string; undefinedFallback?: string
}[] = [ }[] = [
{ {
label: 'Netmask', label: 'Netmask',
getValue: (block) => block.toString(), getValue: block => block.toString(),
}, },
{ {
label: 'Network address', label: 'Network address',
@ -122,4 +83,45 @@ function switchToBlock({ count = 1 }: { count?: number }) {
} }
</script> </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>

View file

@ -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"> <script setup lang="ts">
import { SHA1 } from 'crypto-js'; import { SHA1 } from 'crypto-js';
import InputCopyable from '@/components/InputCopyable.vue'; import InputCopyable from '@/components/InputCopyable.vue';
@ -43,7 +10,7 @@ const calculatedSections = computed(() => {
.toString() .toString()
.substring(30); .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 [ return [
{ {
@ -64,4 +31,35 @@ const calculatedSections = computed(() => {
const addressValidation = macAddressValidation(macAddress); const addressValidation = macAddressValidation(macAddress);
</script> </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>

View file

@ -1,16 +1,16 @@
import _ from 'lodash'; import _ from 'lodash';
import type { ArrayDifference, Difference, ObjectDifference } from '../json-diff.types';
import { useCopy } from '@/composable/copy'; 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 ( return (
<div class={'diffs-viewer'}> <div class={'diffs-viewer'}>
<ul>{DiffViewer({ diff, showKeys: false })}</ul> <ul>{DiffViewer({ diff, showKeys: false })}</ul>
</div> </div>
); );
}; }
const DiffViewer = ({ diff, showKeys = true }: { diff: Difference; showKeys?: boolean }) => { function DiffViewer({ diff, showKeys = true }: { diff: Difference; showKeys?: boolean }) {
const { type, status } = diff; const { type, status } = diff;
if (status === 'updated') { if (status === 'updated') {
@ -26,9 +26,9 @@ const DiffViewer = ({ diff, showKeys = true }: { diff: Difference; showKeys?: bo
} }
return LineDiffViewer({ diff, showKeys }); 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 { value, key, status, oldValue } = diff;
const valueToDisplay = status === 'removed' ? oldValue : value; const valueToDisplay = status === 'removed' ? oldValue : value;
@ -46,9 +46,9 @@ const LineDiffViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boole
, ,
</li> </li>
); );
}; }
const ComparisonViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) => { function ComparisonViewer({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) {
const { value, key, oldValue } = diff; const { value, key, oldValue } = diff;
return ( return (
@ -63,21 +63,21 @@ const ComparisonViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boo
{Value({ value, status: 'added' })}, {Value({ value, status: 'added' })},
</li> </li>
); );
}; }
const ChildrenViewer = ({ function ChildrenViewer({
diff, diff,
openTag, openTag,
closeTag, closeTag,
showKeys, showKeys,
showChildrenKeys = true, showChildrenKeys = true,
}: { }: {
diff: ArrayDifference | ObjectDifference; diff: ArrayDifference | ObjectDifference
showKeys: boolean; showKeys: boolean
showChildrenKeys?: boolean; showChildrenKeys?: boolean
openTag: string; openTag: string
closeTag: string; closeTag: string
}) => { }) {
const { children, key, status, type } = diff; const { children, key, status, type } = diff;
return ( return (
@ -91,12 +91,12 @@ const ChildrenViewer = ({
)} )}
{openTag} {openTag}
{children.length > 0 && <ul>{children.map((diff) => DiffViewer({ diff, showKeys: showChildrenKeys }))}</ul>} {children.length > 0 && <ul>{children.map(diff => DiffViewer({ diff, showKeys: showChildrenKeys }))}</ul>}
{closeTag + ','} {`${closeTag},`}
</div> </div>
</li> </li>
); );
}; }
function formatValue(value: unknown) { function formatValue(value: unknown) {
if (_.isNull(value)) { if (_.isNull(value)) {
@ -106,7 +106,7 @@ function formatValue(value: unknown) {
return JSON.stringify(value); 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 formatedValue = formatValue(value);
const { copy } = useCopy({ source: formatedValue }); const { copy } = useCopy({ source: formatedValue });
@ -116,4 +116,4 @@ const Value = ({ value, status }: { value: unknown; status: string }) => {
{formatedValue} {formatedValue}
</span> </span>
); );
}; }

View file

@ -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> <script lang="ts" setup>
import { useAppTheme } from '@/ui/theme/themes';
import _ from 'lodash'; import _ from 'lodash';
import { DiffRootViewer } from './diff-viewer.models';
import { diff } from '../json-diff.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 props = defineProps<{ leftJson: unknown; rightJson: unknown }>();
const onlyShowDifferences = ref(false);
const { leftJson, rightJson } = toRefs(props); const { leftJson, rightJson } = toRefs(props);
const appTheme = useAppTheme(); const appTheme = useAppTheme();
@ -32,6 +17,23 @@ const jsonAreTheSame = computed(() => _.isEqual(leftJson.value, rightJson.value)
const showResults = computed(() => !_.isUndefined(leftJson.value) && !_.isUndefined(rightJson.value)); const showResults = computed(() => !_.isUndefined(leftJson.value) && !_.isUndefined(rightJson.value));
</script> </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> <style lang="less" scoped>
::v-deep(.diffs-viewer) { ::v-deep(.diffs-viewer) {
color: v-bind('appTheme.text.mutedColor'); color: v-bind('appTheme.text.mutedColor');

View file

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test'; import { expect, test } from '@playwright/test';
test.describe('Tool - JSON diff', () => { test.describe('Tool - JSON diff', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@ -24,7 +24,7 @@ test.describe('Tool - JSON diff', () => {
const result = await page.getByTestId('diff-result').innerText(); 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 }) => { 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(); const result = await page.getByTestId('diff-result').innerText();
expect(result).toContain(`{\nbaz: "qux",\n},`); expect(result).toContain('{\nbaz: "qux",\n},');
}); });
}); });

View file

@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { diff } from './json-diff.models'; import { diff } from './json-diff.models';
describe('json-diff models', () => { describe('json-diff models', () => {

View file

@ -46,8 +46,8 @@ function diffObjects(
): Difference[] { ): Difference[] {
const keys = Object.keys({ ...obj, ...newObj }); const keys = Object.keys({ ...obj, ...newObj });
return keys return keys
.map((key) => createDifference(obj?.[key], newObj?.[key], key, { onlyShowDifferences })) .map(key => createDifference(obj?.[key], newObj?.[key], key, { onlyShowDifferences }))
.filter((diff) => !onlyShowDifferences || diff.status !== 'unchanged'); .filter(diff => !onlyShowDifferences || diff.status !== 'unchanged');
} }
function createDifference( function createDifference(
@ -99,7 +99,7 @@ function diffArrays(
const maxLength = Math.max(0, arr?.length, newArr?.length); const maxLength = Math.max(0, arr?.length, newArr?.length);
return Array.from({ length: maxLength }, (_, i) => return Array.from({ length: maxLength }, (_, i) =>
createDifference(arr?.[i], newArr?.[i], i, { onlyShowDifferences }), 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' { function getType(value: unknown): 'object' | 'array' | 'value' {

View file

@ -1,29 +1,29 @@
export type DifferenceStatus = 'added' | 'removed' | 'updated' | 'unchanged' | 'children-updated'; export type DifferenceStatus = 'added' | 'removed' | 'updated' | 'unchanged' | 'children-updated';
export type ObjectDifference = { export interface ObjectDifference {
key: string | number; key: string | number
type: 'object'; type: 'object'
children: Difference[]; children: Difference[]
status: DifferenceStatus; status: DifferenceStatus
oldValue: unknown; oldValue: unknown
value: unknown; value: unknown
}; }
export type ValueDifference = { export interface ValueDifference {
key: string | number; key: string | number
type: 'value'; type: 'value'
value: unknown; value: unknown
oldValue: unknown; oldValue: unknown
status: DifferenceStatus; status: DifferenceStatus
}; }
export type ArrayDifference = { export interface ArrayDifference {
key: number | string; key: number | string
type: 'array'; type: 'array'
children: Difference[]; children: Difference[]
status: DifferenceStatus; status: DifferenceStatus
oldValue: unknown; oldValue: unknown
value: unknown; value: unknown
}; }
export type Difference = ObjectDifference | ValueDifference | ArrayDifference; export type Difference = ObjectDifference | ValueDifference | ArrayDifference;

View file

@ -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> <template>
<c-input-text <c-input-text
v-model:value="rawLeftJson" v-model:value="rawLeftJson"
@ -23,24 +44,3 @@
<DiffsViewer :left-json="leftJson" :right-json="rightJson" /> <DiffsViewer :left-json="leftJson" :right-json="rightJson" />
</template> </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>

View file

@ -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"> <script setup lang="ts">
import JSON5 from 'json5';
import type { UseValidationRule } from '@/composable/validation'; import type { UseValidationRule } from '@/composable/validation';
import { withDefaultOnError } from '@/utils/defaults'; import { withDefaultOnError } from '@/utils/defaults';
import JSON5 from 'json5';
const defaultValue = '{\n\t"hello": [\n\t\t"world"\n\t]\n}'; const defaultValue = '{\n\t"hello": [\n\t\t"world"\n\t]\n}';
const transformer = (value: string) => withDefaultOnError(() => JSON.stringify(JSON5.parse(value), null, 0), ''); const transformer = (value: string) => withDefaultOnError(() => JSON.stringify(JSON5.parse(value), null, 0), '');
@ -25,3 +13,15 @@ const rules: UseValidationRule<string>[] = [
}, },
]; ];
</script> </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>

View file

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test'; import { expect, test } from '@playwright/test';
test.describe('Tool - json to yaml', () => { test.describe('Tool - json to yaml', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@ -14,6 +14,6 @@ test.describe('Tool - json to yaml', () => {
const generatedJson = await page.getByTestId('area-content').innerText(); 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());
}); });
}); });

View file

@ -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"> <script setup lang="ts">
import { stringify } from 'yaml';
import JSON5 from 'json5';
import type { UseValidationRule } from '@/composable/validation'; import type { UseValidationRule } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean'; import { isNotThrowing } from '@/utils/boolean';
import { withDefaultOnError } from '@/utils/defaults'; import { withDefaultOnError } from '@/utils/defaults';
import { stringify } from 'yaml';
import JSON5 from 'json5';
const transformer = (value: string) => withDefaultOnError(() => stringify(JSON5.parse(value)), ''); const transformer = (value: string) => withDefaultOnError(() => stringify(JSON5.parse(value)), '');
@ -26,4 +15,13 @@ const rules: UseValidationRule<string>[] = [
]; ];
</script> </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>

View file

@ -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> <template>
<div style="flex: 0 0 100%"> <div style="flex: 0 0 100%">
<div style="margin: 0 auto; max-width: 600px" flex justify-center gap-3> <div style="margin: 0 auto; max-width: 600px" flex justify-center gap-3>
@ -28,37 +55,10 @@
/> />
</n-form-item> </n-form-item>
<n-form-item label="Prettify version of your json"> <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> </n-form-item>
</template> </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> <style lang="less" scoped>
.result-card { .result-card {
position: relative; position: relative;

View file

@ -1,4 +1,4 @@
import { get, type MaybeRef } from '@vueuse/core'; import { type MaybeRef, get } from '@vueuse/core';
import JSON5 from 'json5'; import JSON5 from 'json5';
export { sortObjectKeys, formatJson }; export { sortObjectKeys, formatJson };
@ -25,9 +25,9 @@ function formatJson({
sortKeys = true, sortKeys = true,
indentSize = 3, indentSize = 3,
}: { }: {
rawJson: MaybeRef<string>; rawJson: MaybeRef<string>
sortKeys?: MaybeRef<boolean>; sortKeys?: MaybeRef<boolean>
indentSize?: MaybeRef<number>; indentSize?: MaybeRef<number>
}) { }) {
const parsedObject = JSON5.parse(get(rawJson)); const parsedObject = JSON5.parse(get(rawJson));

View file

@ -38,9 +38,11 @@ function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) {
.otherwise(() => undefined); .otherwise(() => undefined);
} }
const dateFormatter = (value: unknown) => { function dateFormatter(value: unknown) {
if (_.isNil(value)) return undefined; if (_.isNil(value)) {
return undefined;
}
const date = new Date(Number(value) * 1000); const date = new Date(Number(value) * 1000);
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
}; }

Some files were not shown because too many files have changed in this diff Show more