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 = {
root: true,
extends: [
'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',
],
extends: ['@antfu', './.eslintrc-auto-import.json', '@unocss'],
settings: {
'import/resolver': { typescript: { project: './tsconfig.app.json' } },
},
env: {
'vue/setup-compiler-macros': true,
},
rules: {
'vue/multi-word-component-names': ['off'],
'prettier/prettier': ['error'],
'import/no-duplicates': ['error', { considerQueryString: true }],
'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }],
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-unresolved': ['error', { ignore: ['^virtual:'] }],
'curly': ['error', 'all'],
'@typescript-eslint/semi': ['error', 'always'],
'@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
'vue/no-empty-component-block': ['error'],
},
};

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']
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.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.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
ColorConverter: typeof import('./src/tools/color-converter/color-converter.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']
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']
DeviceInformation: typeof import('./src/tools/device-information/device-information.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']
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['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']
IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
IconMdiDelete: typeof import('~icons/mdi/delete')['default']
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
IconMdiDeleteOutlined: typeof import('~icons/mdi/delete-outlined')['default']
IconMdiDownload: typeof import('~icons/mdi/download')['default']
IconMdiEye: typeof import('~icons/mdi/eye')['default']
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
IconMdiPause: typeof import('~icons/mdi/pause')['default']
IconMdiPlay: typeof import('~icons/mdi/play')['default']
IconMdiRecord: typeof import('~icons/mdi/record')['default']
IconMdiRecordRec: typeof import('~icons/mdi/record-rec')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconMdiStopCircle: typeof import('~icons/mdi/stop-circle')['default']
IconMdiVideo: typeof import('~icons/mdi/video')['default']
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
@ -103,8 +93,6 @@ declare module '@vue/runtime-core' {
NAlert: typeof import('naive-ui')['NAlert']
NAutoComplete: typeof import('naive-ui')['NAutoComplete']
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']
NCode: typeof import('naive-ui')['NCode']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
@ -116,7 +104,6 @@ declare module '@vue/runtime-core' {
NEllipsis: typeof import('naive-ui')['NEllipsis']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1']
@ -137,7 +124,6 @@ declare module '@vue/runtime-core' {
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSlider: typeof import('naive-ui')['NSlider']
NSpace: typeof import('naive-ui')['NSpace']
NStatistic: typeof import('naive-ui')['NStatistic']
NSwitch: typeof import('naive-ui')['NSwitch']
NTable: typeof import('naive-ui')['NTable']

View file

@ -79,6 +79,7 @@
"yaml": "^2.2.1"
},
"devDependencies": {
"@antfu/eslint-config": "^0.39.3",
"@iconify-json/mdi": "^1.1.50",
"@playwright/test": "^1.32.3",
"@rushstack/eslint-patch": "^1.2.0",
@ -100,18 +101,12 @@
"@vitejs/plugin-vue": "^2.3.4",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"@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/test-utils": "^2.3.2",
"@vue/tsconfig": "^0.1.3",
"c8": "^7.13.0",
"consola": "^3.0.2",
"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",
"less": "^4.1.3",
"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">
import { computed } from 'vue';
import { useRoute, RouterView } from 'vue-router';
import { darkTheme, NGlobalStyle, NMessageProvider, NNotificationProvider } from 'naive-ui';
import { RouterView, useRoute } from 'vue-router';
import { NGlobalStyle, NMessageProvider, NNotificationProvider, darkTheme } from 'naive-ui';
import { darkThemeOverrides, lightThemeOverrides } from './themes';
import { layouts } from './layouts';
import { useStyleStore } from './stores/style.store';
@ -16,14 +16,14 @@ const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrid
<template>
<n-config-provider :theme="theme" :theme-overrides="themeOverrides">
<n-global-style />
<n-message-provider placement="bottom">
<n-notification-provider placement="bottom-right">
<NGlobalStyle />
<NMessageProvider placement="bottom">
<NNotificationProvider placement="bottom-right">
<component :is="layout">
<router-view />
<RouterView />
</component>
</n-notification-provider>
</n-message-provider>
</NNotificationProvider>
</NMessageProvider>
</n-config-provider>
</template>

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>
<div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name">
<n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })">
@ -14,7 +62,7 @@
<n-menu
class="menu"
:value="(route.name as string)"
:value="route.name as string"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="tools"
@ -26,54 +74,6 @@
</div>
</template>
<script setup lang="ts">
import type { Tool, ToolCategory } from '@/tools/tools.types';
import { ChevronRight } from '@vicons/tabler';
import { useStorage } from '@vueuse/core';
import { useThemeVars } from 'naive-ui';
import { toRefs, computed, h } from 'vue';
import { 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">
.category-name {
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>
<c-card class="colored-card">
<n-icon class="icon" size="40" :component="icon" />
@ -13,13 +20,6 @@
</c-card>
</template>
<script setup lang="ts">
import { toRefs, type Component } from 'vue';
const props = defineProps<{ icon: Component; title: string }>();
const { icon, title } = toRefs(props);
</script>
<style lang="less" scoped>
.colored-card {
background: rgb(37, 99, 108);

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">
import { FavoriteFilled } from '@vicons/material';
import { computed, toRefs } from 'vue';
import { useToolStore } from '@/tools/tools.store';
import type { Tool } from '@/tools/tools.types';
import { computed, toRefs } from 'vue';
const props = defineProps<{ tool: Tool }>();
const toolStore = useToolStore();
const props = defineProps<{ tool: Tool }>();
const { tool } = toRefs(props);
const isFavorite = computed(() => toolStore.isToolFavorite({ tool }));
@ -40,3 +24,20 @@ function toggleFavorite(event: MouseEvent) {
toolStore.addToolToFavorites({ tool });
}
</script>
<template>
<n-tooltip trigger="hover">
<template #trigger>
<c-button
variant="text"
circle
:type="buttonType"
:style="{ opacity: isFavorite ? 1 : 0.2 }"
@click="toggleFavorite"
>
<n-icon :component="FavoriteFilled" />
</c-button>
</template>
{{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }}
</n-tooltip>
</template>

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>
<c-input-text
<CInputText
ref="inputElement"
v-model:value="input"
:placeholder="inputPlaceholder"
@ -13,44 +48,9 @@
/>
<div>
<div mb-5px>{{ outputLabel }}</div>
<div mb-5px>
{{ outputLabel }}
</div>
<textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" />
</div>
</template>
<script setup lang="ts">
import type { UseValidationRule } from '@/composable/validation';
import _ from 'lodash';
import 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">
import { useVModel, useClipboard } from '@vueuse/core';
import { useClipboard, useVModel } from '@vueuse/core';
import { ref } from 'vue';
const props = defineProps<{ value: string }>();
@ -34,3 +19,18 @@ function onCopyClicked() {
}, 2000);
}
</script>
<template>
<c-input-text v-model:value="value">
<template #suffix>
<n-tooltip trigger="hover">
<template #trigger>
<c-button circle variant="text" size="small" @click="onCopyClicked">
<icon-mdi-content-copy />
</c-button>
</template>
{{ tooltipText }}
</n-tooltip>
</template>
</c-input-text>
</template>

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

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>
<n-layout has-sider>
<n-layout-sider
@ -19,15 +28,6 @@
</n-layout>
</template>
<script setup lang="ts">
import { useStyleStore } from '@/stores/style.store';
import { toRefs, computed } from 'vue';
const styleStore = useStyleStore();
const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore);
const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static'));
</script>
<style lang="less" scoped>
.overlay {
position: absolute;

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>
<n-tooltip trigger="hover">
<template #trigger>
@ -51,24 +69,6 @@
</n-tooltip>
</template>
<script setup lang="ts">
import { useStyleStore } from '@/stores/style.store';
import { useThemeStore } from '@/ui/theme/theme.store';
import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler';
import { toRefs } from 'vue';
const styleStore = useStyleStore();
const { isDarkTheme } = toRefs(styleStore);
const themeStore = useThemeStore();
function toggleDarkTheme() {
isDarkTheme.value = !isDarkTheme.value;
themeStore.toggleTheme();
}
</script>
<style lang="less" scoped>
.n-button {
&:not(:last-child) {

View file

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

View file

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

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

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>
<div style="overflow-x: hidden; width: 100%">
<c-card class="result-card">
@ -22,57 +68,13 @@
</n-tooltip>
</c-card>
<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>
</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>
::v-deep(.n-scrollbar) {
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>
<router-link :to="tool.path">
<c-card class="tool-card">
@ -16,7 +30,7 @@
New
</n-tag>
<favorite-button :tool="tool" />
<FavoriteButton :tool="tool" />
</div>
</div>
<n-h3 class="title">
@ -26,27 +40,13 @@
<div class="description">
<n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px">
{{ tool.description }}
<br />&nbsp;
<br>&nbsp;
</n-ellipsis>
</div>
</c-card>
</router-link>
</template>
<script setup lang="ts">
import type { Tool } from '@/tools/tools.types';
import { useThemeVars } from 'naive-ui';
import { toRefs } from 'vue';
import { useAppTheme } from '@/ui/theme/themes';
import FavoriteButton from './FavoriteButton.vue';
const props = defineProps<{ tool: Tool & { category: string } }>();
const { tool } = toRefs(props);
const theme = useThemeVars();
const appTheme = useAppTheme();
</script>
<style lang="less" scoped>
a {
text-decoration: none;

View file

@ -11,7 +11,8 @@ function computedRefreshable<T>(getter: () => T, { throttle }: { throttle?: numb
if (throttle) {
watchThrottled(getter, update, { throttle });
} else {
}
else {
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';
export function useCopy({ source, text = 'Copied to the clipboard' }: { source: MaybeRef<unknown>; text?: string }) {

View file

@ -5,8 +5,8 @@ function getFileExtensionFromBase64({
base64String,
defaultExtension = 'txt',
}: {
base64String: string;
defaultExtension?: string;
base64String: string
defaultExtension?: string
}) {
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 { computed } from 'vue';
@ -9,9 +9,9 @@ function useFuzzySearch<Data>({
data,
options = {},
}: {
search: MaybeRef<string>;
data: Data[];
options?: Fuse.IFuseOptions<Data>;
search: MaybeRef<string>
data: Data[]
options?: Fuse.IFuseOptions<Data>
}) {
const fuse = new Fuse(data, options);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useTracker } from '@/modules/tracker/tracker.services';
import { useHead } from '@vueuse/head';
import { useTracker } from '@/modules/tracker/tracker.services';
useHead({ title: 'About - IT Tools' });
const { tracker } = useTracker();
@ -11,7 +11,9 @@ const { tracker } = useTracker();
<n-h1>About</n-h1>
<n-p>
This wonderful website, made with by
<c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener"> Corentin Thomasset </c-link>,
<c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener">
Corentin Thomasset
</c-link>,
aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to share
it to people you think may find it useful too and don't forget to pin it in your shortcut bar !
</n-p>
@ -25,8 +27,8 @@ const { tracker } = useTracker();
target="_blank"
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
>
sponsoring me </c-link
>.
sponsoring me
</c-link>.
</n-p>
<n-h2>Technologies</n-h2>

View file

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

View file

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

View file

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

View file

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

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>
<c-card title="Base64 to file">
<c-input-text
@ -22,63 +70,22 @@
<div mb-2>
<n-icon size="35" :depth="3" :component="Upload" />
</div>
<n-text style="font-size: 14px"> Click or drag a file to this area to upload </n-text>
<n-text style="font-size: 14px">
Click or drag a file to this area to upload
</n-text>
</n-upload-dragger>
</n-upload>
<c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 />
<div flex justify-center>
<c-button @click="copyFileBase64()"> Copy </c-button>
<c-button @click="copyFileBase64()">
Copy
</c-button>
</div>
</c-card>
</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>
::v-deep(.n-upload-trigger) {
width: 100%;

View file

@ -4,7 +4,7 @@ import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Base64 file converter',
path: '/base64-file-converter',
description: "Convert string, files or images into a it's base64 representation.",
description: 'Convert string, files or images into a it\'s base64 representation.',
keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'],
component: () => import('./base64-file-converter.vue'),
icon: FileDigit,

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>
<c-card title="String to base64">
<n-form-item label="Encode URL safe" label-placement="left">
@ -24,7 +51,9 @@
/>
<div flex justify-center>
<c-button @click="copyTextBase64()"> Copy base64 </c-button>
<c-button @click="copyTextBase64()">
Copy base64
</c-button>
</div>
</c-card>
@ -54,34 +83,9 @@
/>
<div flex justify-center>
<c-button @click="copyText()"> Copy decoded string </c-button>
<c-button @click="copyText()">
Copy decoded string
</c-button>
</div>
</c-card>
</template>
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64';
import { withDefaultOnError } from '@/utils/defaults';
import { computed, ref } from 'vue';
const encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false);
const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false);
const textInput = ref('');
const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.value }));
const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' });
const base64Input = ref('');
const textOutput = computed(() =>
withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''),
);
const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' });
const b64ValidationRules = [
{
message: 'Invalid base64 string',
validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }),
},
];
const b64ValidationWatch = [decodeUrlSafe];
</script>

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>
<div>
<c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 />
@ -19,23 +31,13 @@
</n-statistic>
</c-card>
<div mt-5 flex justify-center>
<c-button @click="copy">Copy header</c-button>
<c-button @click="copy">
Copy header
</c-button>
</div>
</div>
</template>
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import { textToBase64 } from '@/utils/base64';
import { computed, ref } from 'vue';
const username = ref('');
const password = ref('');
const header = computed(() => `Authorization: Basic ${textToBase64(`${username.value}:${password.value}`)}`);
const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' });
</script>
<style lang="less" scoped>
::v-deep(.n-statistic-value__content) {
font-family: monospace;

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>
<c-card title="Hash">
<c-input-text
@ -16,7 +34,9 @@
<c-input-text :value="hashed" readonly text-center />
<div mt-5 flex justify-center>
<c-button @click="copy"> Copy hash </c-button>
<c-button @click="copy">
Copy hash
</c-button>
</div>
</c-card>
@ -37,24 +57,6 @@
</c-card>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { hashSync, compareSync } from 'bcryptjs';
import { useCopy } from '@/composable/copy';
import { useThemeVars } from 'naive-ui';
const themeVars = useThemeVars();
const input = ref('');
const saltCount = ref(10);
const hashed = computed(() => hashSync(input.value, saltCount.value));
const { copy } = useCopy({ source: hashed, text: 'Hashed string copied to the clipboard' });
const compareString = ref('');
const compareHash = ref('');
const compareMatch = computed(() => compareSync(compareString.value, compareHash.value));
</script>
<style lang="less" scoped>
.compare-result {
color: v-bind('themeVars.errorColor');

View file

@ -13,7 +13,7 @@ function computeAverage({ data }: { data: number[] }) {
function computeVariance({ data }: { data: number[] }) {
const mean = computeAverage({ data });
const squaredDiffs = data.map((value) => Math.pow(value - mean, 2));
const squaredDiffs = data.map(value => (value - mean) ** 2);
return computeAverage({ data: squaredDiffs });
}
@ -24,11 +24,11 @@ function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; heade
}
const headers = Object.keys(data[0]);
const rows = data.map((obj) => Object.values(obj));
const rows = data.map(obj => Object.values(obj));
const headerRow = `| ${headers.map((header) => headerMap[header] ?? header).join(' | ')} |`;
const headerRow = `| ${headers.map(header => headerMap[header] ?? header).join(' | ')} |`;
const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`;
const dataRows = rows.map((row) => `| ${row.join(' | ')} |`).join('\n');
const dataRows = rows.map(row => `| ${row.join(' | ')} |`).join('\n');
return `${headerRow}\n${separatorRow}\n${dataRows}`;
}

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">
import { Trash, Plus } from '@vicons/tabler';
import { Plus, Trash } from '@vicons/tabler';
import { useClipboard, useStorage } from '@vueuse/core';
import _ from 'lodash';
import { computed } from 'vue';
import { computeAverage, computeVariance, arrayToMarkdownTable } from './benchmark-builder.models';
import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models';
import DynamicValues from './dynamic-values.vue';
const suites = useStorage('benchmark-builder:suites', [
@ -114,8 +34,8 @@ const results = computed(() => {
const deltaWithBestMean = mean - bestMean;
const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean);
const comparisonValues: string =
index !== 0 && bestMean !== mean ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
const comparisonValues: string
= (index !== 0 && bestMean !== mean) ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
return {
position: index + 1,
@ -157,4 +77,87 @@ function copyAsBulletList() {
}
</script>
<style lang="less" scoped></style>
<template>
<n-scrollbar style="flex: 1" x-scrollable>
<div mb-5 flex flex-1 flex-nowrap justify-center gap-12px>
<div v-for="(suite, index) of suites" :key="index">
<c-card style="width: 294px">
<c-input-text
v-model:value="suite.title"
label-position="left"
label="Suite name"
placeholder="Suite name..."
clearable
/>
<n-divider />
<n-form-item label="Suite values" :show-feedback="false">
<DynamicValues v-model:values="suite.data" />
</n-form-item>
</c-card>
<div flex justify-center>
<c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)">
<n-icon :component="Trash" depth="3" mr-2 size="18" />
Delete suite
</c-button>
<c-button
variant="text"
@click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })"
>
<n-icon :component="Plus" depth="3" mr-2 size="18" />
Add suite
</c-button>
</div>
</div>
</div>
</n-scrollbar>
<div style="flex: 0 0 100%">
<div style="max-width: 600px; margin: 0 auto">
<div mx-auto max-w-sm flex justify-center gap-3>
<c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 />
<c-button
@click="
suites = [
{ title: 'Suite 1', data: [] },
{ title: 'Suite 2', data: [] },
]
"
>
Reset suites
</c-button>
</div>
<n-table>
<thead>
<tr>
<th>{{ header.position }}</th>
<th>{{ header.title }}</th>
<th>{{ header.size }}</th>
<th>{{ header.mean }}</th>
<th>{{ header.variance }}</th>
</tr>
</thead>
<tbody>
<tr v-for="{ title, size, mean, variance, position } of results" :key="title">
<td>{{ position }}</td>
<td>{{ title }}</td>
<td>{{ size }}</td>
<td>{{ mean }}</td>
<td>{{ variance }}</td>
</tr>
</tbody>
</n-table>
<div mt-5 flex justify-center gap-3>
<c-button @click="copyAsMarkdown">
Copy as markdown table
</c-button>
<c-button @click="copyAsBulletList">
Copy as bullet list
</c-button>
</div>
</div>
</div>
</template>

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>
<div>
<div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2>
<n-input-number
<NInputNumber
:ref="refs.set"
v-model:value="values[index]"
:show-button="false"
@ -25,33 +55,3 @@
</c-button>
</div>
</template>
<script setup lang="ts">
import { Trash, Plus } from '@vicons/tabler';
import { useTemplateRefsList, useVModel } from '@vueuse/core';
import { NInputNumber } from 'naive-ui';
import { nextTick } from 'vue';
const refs = useTemplateRefsList<typeof NInputNumber>();
const props = defineProps<{ values: (number | null)[] }>();
const emit = defineEmits(['update:values']);
const values = useVModel(props, 'values', emit);
async function addValue() {
values.value.push(null);
await nextTick();
refs.value.at(-1)?.focus();
}
function onInputEnter(index: number) {
if (index === values.value.length - 1) {
addValue();
return;
}
refs.value.at(index + 1)?.focus();
}
</script>
<style scoped></style>

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>
<div>
<n-grid cols="3" x-gap="12">
@ -47,85 +129,3 @@
</n-form-item>
</div>
</template>
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import { useValidation } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean';
import { withDefaultOnError } from '@/utils/defaults';
import {
chineseSimplifiedWordList,
chineseTraditionalWordList,
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">
import _ from 'lodash';
import { useMediaRecorder } from './useMediaRecorder';
type Media = { type: 'image' | 'video'; value: string; createdAt: Date };
interface Media { type: 'image' | 'video'; value: string; createdAt: Date }
const {
videoInputs: cameras,
@ -156,19 +54,19 @@ onRecordAvailable((value) => {
});
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;
}
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;
}
}
function takeScreenshot() {
if (!video.value) return;
if (!video.value) {
return;
}
const canvas = document.createElement('canvas');
canvas.width = video.value.videoWidth;
@ -180,13 +78,16 @@ function takeScreenshot() {
}
watchEffect(() => {
if (video.value && stream.value) video.value.srcObject = stream.value;
if (video.value && stream.value) {
video.value.srcObject = stream.value;
}
});
async function requestPermissions() {
try {
await ensurePermissions();
} catch (e) {
}
catch (e) {
permissionCannotBePrompted.value = true;
}
}
@ -199,4 +100,114 @@ function downloadMedia({ type, value, createdAt }: Media) {
}
</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 };
function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): {
isRecordingSupported: Ref<boolean>;
recordingState: Ref<'stopped' | 'recording' | 'paused'>;
startRecording: () => void;
stopRecording: () => void;
pauseRecording: () => void;
resumeRecording: () => void;
onRecordAvailable: (cb: (url: string) => void) => void;
isRecordingSupported: Ref<boolean>
recordingState: Ref<'stopped' | 'recording' | 'paused'>
startRecording: () => void
stopRecording: () => void
pauseRecording: () => void
resumeRecording: () => void
onRecordAvailable: (cb: (url: string) => void) => void
} {
const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm'));
const mediaRecorder = ref<MediaRecorder | null>(null);
@ -17,10 +17,23 @@ function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }):
const recordAvailable = createEventHook();
const recordingState = ref<'stopped' | 'recording' | 'paused'>('stopped');
const createVideo = () => {
const blob = new Blob(recordedChunks.value, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
recordedChunks.value = [];
return url;
};
const startRecording = () => {
if (!isRecordingSupported.value) return;
if (!stream.value) return;
if (recordingState.value !== 'stopped') return;
if (!isRecordingSupported.value) {
return;
}
if (!stream.value) {
return;
}
if (recordingState.value !== 'stopped') {
return;
}
mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' });
@ -34,47 +47,60 @@ function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }):
recordAvailable.trigger(createVideo());
};
if (mediaRecorder.value.state !== 'inactive') return;
if (mediaRecorder.value.state !== 'inactive') {
return;
}
mediaRecorder.value.start();
recordingState.value = 'recording';
};
const stopRecording = () => {
if (!isRecordingSupported.value) return;
if (!mediaRecorder.value) return;
if (recordingState.value === 'stopped') return;
if (!isRecordingSupported.value) {
return;
}
if (!mediaRecorder.value) {
return;
}
if (recordingState.value === 'stopped') {
return;
}
mediaRecorder.value.stop();
recordingState.value = 'stopped';
};
const pauseRecording = () => {
if (!isRecordingSupported.value) return;
if (!mediaRecorder.value) return;
if (recordingState.value !== 'recording') return;
if (!isRecordingSupported.value) {
return;
}
if (!mediaRecorder.value) {
return;
}
if (recordingState.value !== 'recording') {
return;
}
mediaRecorder.value.pause();
recordingState.value = 'paused';
};
const resumeRecording = () => {
if (!isRecordingSupported.value) return;
if (!mediaRecorder.value) return;
if (!isRecordingSupported.value) {
return;
}
if (!mediaRecorder.value) {
return;
}
if (recordingState.value !== 'paused') return;
if (recordingState.value !== 'paused') {
return;
}
mediaRecorder.value.resume();
recordingState.value = 'recording';
};
const createVideo = () => {
const blob = new Blob(recordedChunks.value, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
recordedChunks.value = [];
return url;
};
return {
isRecordingSupported,
startRecording,

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">
import { ref } from 'vue';
import {
@ -74,6 +22,58 @@ const baseConfig = {
const input = ref('lorem ipsum dolor sit amet');
</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>
.n-form-item {
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';
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">
import { useThemeVars } from 'naive-ui';
import { computed, ref } from 'vue';
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
import InputCopyable from '../../components/InputCopyable.vue';
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
import type { Group, Scope } from './chmod-calculator.types';
@ -54,6 +24,44 @@ const permissions = ref({
const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
</script>
<template>
<div>
<n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
<thead>
<tr>
<th class="text-center" scope="col" />
<th class="text-center" scope="col">
Owner (u)
</th>
<th class="text-center" scope="col">
Group (g)
</th>
<th class="text-center" scope="col">
Public (o)
</th>
</tr>
</thead>
<tbody>
<tr v-for="{ scope, title } of scopes" :key="scope">
<td class="line-header">
{{ title }}
</td>
<td v-for="group of groups" :key="group" class="text-center">
<!-- <n-switch v-model:value="permissions[group][scope]" /> -->
<n-checkbox v-model:checked="permissions[group][scope]" size="large" />
</td>
</tr>
</tbody>
</n-table>
<div class="octal-result">
{{ octal }}
</div>
<InputCopyable :value="`chmod ${octal} path`" readonly />
</div>
</template>
<style lang="less" scoped>
.octal-result {
text-align: center;

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">
import { useRafFn } from '@vueuse/core';
import { ref } from 'vue';
@ -42,6 +28,28 @@ function pause() {
}
</script>
<template>
<div>
<c-card>
<div class="duration">
{{ formatMs(counter) }}
</div>
</c-card>
<div mt-5 flex justify-center gap-3>
<c-button v-if="!isRunning" type="primary" @click="resume">
Start
</c-button>
<c-button v-else type="warning" @click="pause">
Stop
</c-button>
<c-button @click="counter = 0">
Reset
</c-button>
</div>
</div>
</template>
<style lang="less" scoped>
.duration {
text-align: center;

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">
import { ref } from 'vue';
import { colord, extend } from 'colord';
@ -57,17 +22,67 @@ function onInputUpdated(value: string, omit: string) {
try {
const color = colord(value);
if (omit !== 'name') name.value = color.toName({ closest: true }) ?? '';
if (omit !== 'hex') hex.value = color.toHex();
if (omit !== 'rgb') rgb.value = color.toRgbString();
if (omit !== 'hsl') hsl.value = color.toHslString();
if (omit !== 'hwb') hwb.value = color.toHwbString();
if (omit !== 'cmyk') cmyk.value = color.toCmykString();
if (omit !== 'lch') lch.value = color.toLchString();
} catch {
if (omit !== 'name') {
name.value = color.toName({ closest: true }) ?? '';
}
if (omit !== 'hex') {
hex.value = color.toHex();
}
if (omit !== 'rgb') {
rgb.value = color.toRgbString();
}
if (omit !== 'hsl') {
hsl.value = color.toHslString();
}
if (omit !== 'hwb') {
hwb.value = color.toHwbString();
}
if (omit !== 'cmyk') {
cmyk.value = color.toCmykString();
}
if (omit !== 'lch') {
lch.value = color.toLchString();
}
}
catch {
//
}
}
onInputUpdated(hex.value, 'hex');
</script>
<template>
<c-card>
<n-form label-width="100" label-placement="left">
<n-form-item label="color picker:">
<n-color-picker
v-model:value="hex"
placement="bottom-end"
@update:value="(v: string) => onInputUpdated(v, 'hex')"
/>
</n-form-item>
<n-form-item label="color name:">
<InputCopyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" />
</n-form-item>
<n-form-item label="hex:">
<InputCopyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" />
</n-form-item>
<n-form-item label="rgb:">
<InputCopyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" />
</n-form-item>
<n-form-item label="hsl:">
<InputCopyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" />
</n-form-item>
<n-form-item label="hwb:">
<InputCopyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" />
</n-form-item>
<n-form-item label="lch:">
<InputCopyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" />
</n-form-item>
<n-form-item label="cmyk:">
<InputCopyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" />
</n-form-item>
</n-form>
</c-card>
</template>

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">
import cronstrue from 'cronstrue';
import { isValidCron } from 'cron-validator';
@ -194,6 +108,97 @@ const cronValidationRules = [
];
</script>
<template>
<c-card>
<div mx-auto max-w-sm>
<c-input-text
v-model:value="cron"
size="large"
placeholder="* * * * *"
:validation-rules="cronValidationRules"
mb-3
/>
</div>
<div class="cron-string">
{{ cronString }}
</div>
<n-divider />
<div flex justify-center>
<n-form :show-feedback="false" label-width="170" label-placement="left">
<n-form-item label="Verbose">
<n-switch v-model:value="cronstrueConfig.verbose" />
</n-form-item>
<n-form-item label="Use 24 hour time format">
<n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" />
</n-form-item>
<n-form-item label="Days start at 0">
<n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" />
</n-form-item>
</n-form>
</div>
</c-card>
<c-card>
<pre>
[optional] seconds (0 - 59)
| minute (0 - 59)
| | hour (0 - 23)
| | | day of month (1 - 31)
| | | | month (1 - 12) OR jan,feb,mar,apr ...
| | | | | day of week (0 - 6, sunday=0) OR sun,mon ...
| | | | | |
* * * * * * command</pre>
<div v-if="styleStore.isSmallScreen">
<c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none>
<div>
Symbol: <strong>{{ symbol }}</strong>
</div>
<div>
Meaning: <strong>{{ meaning }}</strong>
</div>
<div>
Example:
<strong><code>{{ example }}</code></strong>
</div>
<div>
Equivalent: <strong>{{ equivalent }}</strong>
</div>
</c-card>
</div>
<n-table v-else size="small">
<thead>
<tr>
<th class="text-left" scope="col">
Symbol
</th>
<th class="text-left" scope="col">
Meaning
</th>
<th class="text-left" scope="col">
Example
</th>
<th class="text-left" scope="col">
Equivalent
</th>
</tr>
</thead>
<tbody>
<tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol">
<td>{{ symbol }}</td>
<td>{{ meaning }}</td>
<td>
<code>{{ example }}</code>
</td>
<td>{{ equivalent }}</td>
</tr>
</tbody>
</n-table>
</c-card>
</template>
<style lang="less" scoped>
::v-deep(input) {
font-size: 30px;

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.beforeEach(async ({ page }) => {

View file

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

View file

@ -11,13 +11,13 @@ export {
isMongoObjectId,
};
const ISO8601_REGEX =
/^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
const ISO9075_REGEX =
/^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/;
const ISO8601_REGEX
= /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
const ISO9075_REGEX
= /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/;
const RFC3339_REGEX =
/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/;
const RFC3339_REGEX
= /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/;
const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/;
@ -40,7 +40,8 @@ function isUTCDateString(date?: string) {
try {
return new Date(date).toUTCString() === date;
} catch (_ignored) {
}
catch (_ignored) {
return false;
}
}

View file

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

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>
<div>
<n-input-group>
@ -36,142 +178,3 @@
/>
</div>
</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">
import { useWindowSize } from '@vueuse/core';
import { computed } from 'vue';
@ -77,6 +58,27 @@ const sections = [
];
</script>
<template>
<c-card v-for="{ name, information } in sections" :key="name" :title="name">
<n-grid cols="1 400:2" x-gap="12" y-gap="12">
<n-gi v-for="{ label, value: { value } } in information" :key="label" class="information">
<div class="label">
{{ label }}
</div>
<div class="value">
<n-ellipsis v-if="value">
{{ value }}
</n-ellipsis>
<div v-else class="undefined-value">
unknown
</div>
</div>
</n-gi>
</n-grid>
</c-card>
</template>
<style lang="less" scoped>
.information {
padding: 14px 16px;

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>
<div>
<n-form-item label="Your docker run command:" :show-feedback="false">
@ -12,16 +43,20 @@
<n-divider />
<textarea-copyable :value="dockerCompose" language="yaml" />
<TextareaCopyable :value="dockerCompose" language="yaml" />
<div mt-5 flex justify-center>
<c-button :disabled="dockerCompose === ''" secondary @click="download"> Download docker-compose.yml </c-button>
<c-button :disabled="dockerCompose === ''" secondary @click="download">
Download docker-compose.yml
</c-button>
</div>
<div v-if="notComposable.length > 0">
<n-alert title="This options are not translatable to docker-compose" type="info" mt-5>
<ul>
<li v-for="(message, index) of notComposable" :key="index">{{ message }}</li>
<li v-for="(message, index) of notComposable" :key="index">
{{ message }}
</li>
</ul>
</n-alert>
</div>
@ -33,7 +68,9 @@
mt-5
>
<ul>
<li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li>
<li v-for="(message, index) of notImplemented" :key="index">
{{ message }}
</li>
</ul>
</n-alert>
</div>
@ -41,41 +78,11 @@
<div v-if="errors.length > 0">
<n-alert title="The following errors occured" type="error" mt-5>
<ul>
<li v-for="(message, index) of errors" :key="index">{{ message }}</li>
<li v-for="(message, index) of errors" :key="index">
{{ message }}
</li>
</ul>
</n-alert>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { withDefaultOnError } from '@/utils/defaults';
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
import { textToBase64 } from '@/utils/base64';
import TextareaCopyable from '@/components/TextareaCopyable.vue';
import { composerize, MessageType } from 'composerize-ts';
const dockerRun = ref(
'docker run -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro --restart always --log-opt max-size=1g nginx',
);
const conversionResult = computed(() =>
withDefaultOnError(() => composerize(dockerRun.value), { yaml: '', messages: [] }),
);
const dockerCompose = computed(() => conversionResult.value.yaml);
const notImplemented = computed(() =>
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notImplemented).map((msg) => msg.value),
);
const notComposable = computed(() =>
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notTranslatable).map((msg) => msg.value),
);
const errors = computed(() =>
conversionResult.value.messages
.filter((msg) => msg.type === MessageType.errorDuringConversion)
.map((msg) => msg.value),
);
const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value));
const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' });
</script>

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>
<c-card title="Encrypt">
<div flex gap-3>
@ -78,22 +97,3 @@
</n-form-item>
</c-card>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { AES, TripleDES, Rabbit, RC4, enc } from 'crypto-js';
const algos = { AES, TripleDES, Rabbit, RC4 };
const cypherInput = ref('Lorem ipsum dolor sit amet');
const cypherAlgo = ref<keyof typeof algos>('AES');
const cypherSecret = ref('my secret key');
const cypherOutput = computed(() => algos[cypherAlgo.value].encrypt(cypherInput.value, cypherSecret.value).toString());
const decryptInput = ref('U2FsdGVkX1/EC3+6P5dbbkZ3e1kQ5o2yzuU0NHTjmrKnLBEwreV489Kr0DIB+uBs');
const decryptAlgo = ref<keyof typeof algos>('AES');
const decryptSecret = ref('my secret key');
const decryptOutput = computed(() =>
algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8),
);
</script>

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>
<div>
<n-text depth="3" style="text-align: justify; width: 100%; display: inline-block">
@ -29,45 +54,24 @@
{ label: 'hours', value: 1000 * 60 * 60 },
{ label: 'days', value: 1000 * 60 * 60 * 24 },
]"
></n-select>
/>
</n-input-group>
</n-form-item>
<n-divider />
<c-card mb-2>
<n-statistic label="Total duration">{{ formatMsDuration(durationMs) }}</n-statistic>
<n-statistic label="Total duration">
{{ formatMsDuration(durationMs) }}
</n-statistic>
</c-card>
<c-card>
<n-statistic label="It will end ">{{ endAt }}</n-statistic>
<n-statistic label="It will end ">
{{ endAt }}
</n-statistic>
</c-card>
</div>
</template>
<script setup lang="ts">
// Duplicate issue with sub directory
// eslint-disable-next-line import/no-duplicates
import { addMilliseconds, formatRelative } from 'date-fns';
// eslint-disable-next-line import/no-duplicates
import { enGB } from 'date-fns/locale';
import { computed, ref } from 'vue';
import { formatMsDuration } from './eta-calculator.service';
const unitCount = ref(3 * 62);
const unitPerTimeSpan = ref(3);
const timeSpan = ref(5);
const timeSpanUnitMultiplier = ref(60000);
const startedAt = ref(Date.now());
const durationMs = computed(() => {
const timeSpanMs = timeSpan.value * timeSpanUnitMultiplier.value;
return unitCount.value / (unitPerTimeSpan.value / timeSpanMs);
});
const endAt = computed(() =>
formatRelative(addMilliseconds(startedAt.value, durationMs.value), Date.now(), { locale: enGB }),
);
</script>
<style lang="less" scoped>
.n-input-number,
.n-date-picker {

View file

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

View file

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

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>
<div>
<c-card>
@ -31,45 +67,12 @@
<div v-for="algo in algoNames" :key="algo" style="margin: 5px 0">
<n-input-group>
<n-input-group-label style="flex: 0 0 120px"> {{ algo }} </n-input-group-label>
<input-copyable :value="hashText(algo, clearText)" readonly />
<n-input-group-label style="flex: 0 0 120px">
{{ algo }}
</n-input-group-label>
<InputCopyable :value="hashText(algo, clearText)" readonly />
</n-input-group>
</div>
</c-card>
</div>
</template>
<script setup lang="ts">
import { useQueryParam } from '@/composable/queryParams';
import { enc, lib, MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512 } from 'crypto-js';
import { ref } from 'vue';
import InputCopyable from '../../components/InputCopyable.vue';
import { convertHexToBin } from './hash-text.service';
const algos = {
MD5,
SHA1,
SHA256,
SHA224,
SHA512,
SHA384,
SHA3,
RIPEMD160,
} as const;
type AlgoNames = keyof typeof algos;
type Encoding = keyof typeof enc | 'Bin';
const algoNames = Object.keys(algos) as AlgoNames[];
const encoding = useQueryParam<Encoding>({ defaultValue: 'Hex', name: 'encoding' });
const clearText = ref('');
function formatWithEncoding(words: lib.WordArray, encoding: Encoding) {
if (encoding === 'Bin') {
return convertHexToBin(words.toString(enc.Hex));
}
return words.toString(enc[encoding]);
}
const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[algo](value), encoding.value);
</script>

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>
<div>
<n-form-item label="Plain text to compute the hash">
@ -43,54 +90,9 @@
<n-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." />
</n-form-item>
<div flex justify-center>
<c-button @click="copy()">Copy HMAC</c-button>
<c-button @click="copy()">
Copy HMAC
</c-button>
</div>
</div>
</template>
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import {
enc,
HmacMD5,
HmacRIPEMD160,
HmacSHA1,
HmacSHA224,
HmacSHA256,
HmacSHA3,
HmacSHA384,
HmacSHA512,
lib,
} from 'crypto-js';
import { computed, ref } from 'vue';
import { convertHexToBin } from '../hash-text/hash-text.service';
const algos = {
MD5: HmacMD5,
RIPEMD160: HmacRIPEMD160,
SHA1: HmacSHA1,
SHA3: HmacSHA3,
SHA224: HmacSHA224,
SHA256: HmacSHA256,
SHA384: HmacSHA384,
SHA512: HmacSHA512,
} as const;
type Encoding = keyof typeof enc | 'Bin';
function formatWithEncoding(words: lib.WordArray, encoding: Encoding) {
if (encoding === 'Bin') {
return convertHexToBin(words.toString(enc.Hex));
}
return words.toString(enc[encoding]);
}
const plainText = ref('');
const secret = ref('');
const hashFunction = ref<keyof typeof algos>('SHA256');
const encoding = ref<Encoding>('Hex');
const hmac = computed(() =>
formatWithEncoding(algos[hashFunction.value](plainText.value, secret.value), encoding.value),
);
const { copy } = useCopy({ source: hmac });
</script>

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>
<c-card title="Escape html entities">
<n-form-item label="Your string :">
@ -20,7 +34,9 @@
</n-form-item>
<div flex justify-center>
<c-button @click="copyEscaped"> Copy </c-button>
<c-button @click="copyEscaped">
Copy
</c-button>
</div>
</c-card>
<c-card title="Unescape html entities">
@ -44,21 +60,9 @@
</n-form-item>
<div flex justify-center>
<c-button @click="copyUnescaped"> Copy </c-button>
<c-button @click="copyUnescaped">
Copy
</c-button>
</div>
</c-card>
</template>
<script setup lang="ts">
import { escape, unescape } from 'lodash';
import { computed, ref } from 'vue';
import { useCopy } from '@/composable/copy';
const escapeInput = ref('<title>IT Tool</title>');
const escapeOutput = computed(() => escape(escapeInput.value));
const { copy: copyEscaped } = useCopy({ source: escapeOutput });
const unescapeInput = ref('&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">
import { tryOnBeforeUnmount, useVModel } from '@vueuse/core';
import { Editor, EditorContent } from '@tiptap/vue-3';
@ -16,9 +5,9 @@ import StarterKit from '@tiptap/starter-kit';
import { useThemeVars } from 'naive-ui';
import MenuBar from './menu-bar.vue';
const themeVars = useThemeVars();
const props = defineProps<{ html: string }>();
const emit = defineEmits(['update:html']);
const themeVars = useThemeVars();
const html = useVModel(props, 'html', emit);
const editor = new Editor({
@ -33,6 +22,17 @@ tryOnBeforeUnmount(() => {
});
</script>
<template>
<c-card v-if="editor" important:p0>
<MenuBar class="editor-header" :editor="editor" />
<n-divider style="margin-top: 0" />
<div px8 pb6>
<EditorContent class="editor-content" :editor="editor" />
</div>
</c-card>
</template>
<style scoped lang="less">
::v-deep(.ProseMirror-focused) {
outline: none;

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>
<n-tooltip trigger="hover">
<template #trigger>
@ -9,12 +16,3 @@
{{ title }}
</n-tooltip>
</template>
<script setup lang="ts">
import { toRefs, type Component } from 'vue';
const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean }>();
const { icon, title, action, isActive } = toRefs(props);
</script>
<style scoped></style>

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">
import type { Editor } from '@tiptap/vue-3';
import {
@ -27,7 +18,7 @@ import {
Strikethrough,
TextWrap,
} from '@vicons/tabler';
import { toRefs, type Component } from 'vue';
import { type Component, toRefs } from 'vue';
import MenuBarItem from './menu-bar-item.vue';
const props = defineProps<{ editor: Editor }>();
@ -35,11 +26,11 @@ const { editor } = toRefs(props);
type MenuItem =
| {
icon: Component;
title: string;
action: () => void;
isActive?: () => boolean;
type: 'button';
icon: Component
title: string
action: () => void
isActive?: () => boolean
type: 'button'
}
| { type: 'divider' };
@ -166,4 +157,11 @@ const items: MenuItem[] = [
];
</script>
<style scoped></style>
<template>
<div flex items-center>
<template v-for="(item, index) in items">
<n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical />
<MenuBarItem v-else-if="item.type === 'button'" :key="index" v-bind="item" />
</template>
</div>
</template>

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

View file

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

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.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>
<div>
<n-form-item :show-label="false">
@ -21,35 +45,13 @@
<n-h2> {{ category }} </n-h2>
<c-card v-for="{ code, description, name, type } of codes" :key="code" mb-2>
<n-text strong block text-lg> {{ code }} {{ name }} </n-text>
<n-text block depth="3">{{ description }} {{ type !== 'HTTP' ? `For ${type}.` : '' }}</n-text>
<n-text strong block text-lg>
{{ code }} {{ name }}
</n-text>
<n-text block depth="3">
{{ description }} {{ type !== 'HTTP' ? `For ${type}.` : '' }}
</n-text>
</c-card>
</div>
</div>
</template>
<script setup lang="ts">
import { useFuzzySearch } from '@/composable/fuzzySearch';
import { SearchRound } from '@vicons/material';
import { codesByCategories } from './http-status-codes.constants';
const search = ref('');
const { searchResult } = useFuzzySearch({
search,
data: codesByCategories.flatMap(({ codes, category }) => codes.map((code) => ({ ...code, category }))),
options: {
keys: [{ name: 'code', weight: 3 }, { name: 'name', weight: 2 }, 'description', 'category'],
},
});
const codesByCategoryFiltered = computed(() => {
if (!search.value) {
return codesByCategories;
}
return [{ category: 'Search results', codes: searchResult.value }];
});
</script>
<style lang="less" scoped></style>

View file

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

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';
describe('integer-base-converter', () => {

View file

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

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>
<div>
<c-card>
<div v-if="styleStore.isSmallScreen">
<n-input-group>
<n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
<n-input-group-label style="flex: 0 0 120px">
Input number:
</n-input-group-label>
<n-input v-model:value="input" w-full :status="error ? 'error' : undefined" />
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label>
<n-input-group-label style="flex: 0 0 120px">
Input base:
</n-input-group-label>
<n-input-number v-model:value="inputBase" max="64" min="2" w-full />
</n-input-group>
</div>
<n-input-group v-else>
<n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
<n-input-group-label style="flex: 0 0 120px">
Input number:
</n-input-group-label>
<n-input v-model:value="input" :status="error ? 'error' : undefined" />
<n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label>
<n-input-group-label style="flex: 0 0 120px">
Input base:
</n-input-group-label>
<n-input-number v-model:value="inputBase" max="64" min="2" />
</n-input-group>
<n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert>
<n-alert v-if="error" style="margin-top: 25px" type="error">
{{ error }}
</n-alert>
<n-divider />
<input-copyable
<InputCopyable
label="Binary (2)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
placeholder="Binary version will be here..."
/>
<input-copyable
<InputCopyable
label="Octal (8)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
placeholder="Octal version will be here..."
/>
<input-copyable
<InputCopyable
label="Decimal (10)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
placeholder="Decimal version will be here..."
/>
<input-copyable
<InputCopyable
label="Hexadecimal (16)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })"
placeholder="Hexadecimal version will be here..."
/>
<input-copyable
<InputCopyable
label="Base64 (64)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })"
@ -63,7 +110,7 @@
<n-input-number v-model:value="outputBase" max="64" min="2" />
</n-input-group>
<input-copyable
<InputCopyable
flex-1
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })"
@ -74,42 +121,6 @@
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useStyleStore } from '@/stores/style.store';
import { getErrorMessageIfThrows } from '@/utils/error';
import { convertBase } from './integer-base-converter.model';
import InputCopyable from '../../components/InputCopyable.vue';
const styleStore = useStyleStore();
const inputProps = {
labelPosition: 'left',
labelWidth: '170px',
labelAlign: 'right',
readonly: true,
'mb-2': '',
} as const;
const input = ref('42');
const inputBase = ref(10);
const outputBase = ref(42);
function errorlessConvert(...args: Parameters<typeof convertBase>) {
try {
return convertBase(...args);
} catch (err) {
return '';
}
}
const error = computed(() =>
getErrorMessageIfThrows(() =>
convertBase({ value: input.value, fromBase: inputBase.value, toBase: outputBase.value }),
),
);
</script>
<style lang="less" scoped>
.n-input-group:not(:first-child) {
margin-top: 5px;

View file

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

View file

@ -10,7 +10,7 @@ function ipv4ToInt({ ip }: { ip: string }) {
return ip
.trim()
.split('.')
.reduce((acc, part, index) => acc + Number(part) * Math.pow(256, 3 - index), 0);
.reduce((acc, part, index) => acc + Number(part) * 256 ** (3 - index), 0);
}
function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: string; prefix?: string }) {
@ -19,13 +19,13 @@ function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: str
}
return (
prefix +
_.chain(ip)
prefix
+ _.chain(ip)
.trim()
.split('.')
.map((part) => parseInt(part).toString(16).padStart(2, '0'))
.map(part => parseInt(part).toString(16).padStart(2, '0'))
.chunk(2)
.map((blocks) => blocks.join(''))
.map(blocks => blocks.join(''))
.join(':')
.value()
);

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">
import { useValidation } from '@/composable/validation';
import { convertBase } from '../integer-base-converter/integer-base-converter.model';
import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from './ipv4-address-converter.service';
import { useValidation } from '@/composable/validation';
const rawIpAddress = useStorage('ipv4-converter:ip', '192.168.1.1');
@ -54,8 +34,26 @@ const convertedSections = computed(() => {
const { attrs: validationAttrs } = useValidation({
source: rawIpAddress,
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }],
rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }],
});
</script>
<style lang="less" scoped></style>
<template>
<div>
<c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." />
<n-divider />
<input-copyable
v-for="{ label, value } of convertedSections"
:key="label"
:label="label"
label-position="left"
label-width="100px"
label-align="right"
mb-2
:value="validationAttrs.validationStatus === 'error' ? '' : value"
placeholder="Set a correct ipv4 address"
/>
</div>
</template>

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

View file

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

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>
<div>
<div mb-4 flex gap-4>
@ -21,13 +79,19 @@
<n-table v-if="showResult" data-test-id="result">
<thead>
<tr>
<th scope="col">&nbsp;</th>
<th scope="col">old value</th>
<th scope="col">new value</th>
<th scope="col">
&nbsp;
</th>
<th scope="col">
old value
</th>
<th scope="col">
new value
</th>
</tr>
</thead>
<tbody>
<result-row
<ResultRow
v-for="{ label, getOldValue, getNewValue } in calculatedValues"
:key="label"
:label="label"
@ -53,62 +117,3 @@
</n-alert>
</div>
</template>
<script setup lang="ts">
import { useValidation } from '@/composable/validation';
import { Exchange } from '@vicons/tabler';
import { isValidIpv4 } from '../ipv4-address-converter/ipv4-address-converter.service';
import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types';
import { calculateCidr } from './ipv4-range-expander.service';
import ResultRow from './result-row.vue';
const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1');
const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255');
const result = computed(() => calculateCidr({ startIp: rawStartAddress.value, endIp: rawEndAddress.value }));
const calculatedValues: {
label: string;
getOldValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined;
getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined;
}[] = [
{
label: 'Start address',
getOldValue: () => rawStartAddress.value,
getNewValue: (result) => result?.newStart,
},
{
label: 'End address',
getOldValue: () => rawEndAddress.value,
getNewValue: (result) => result?.newEnd,
},
{
label: 'Addresses in range',
getOldValue: (result) => result?.oldSize?.toLocaleString(),
getNewValue: (result) => result?.newSize?.toLocaleString(),
},
{
label: 'CIDR',
getOldValue: () => '',
getNewValue: (result) => result?.newCidr,
},
];
const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined);
const startIpValidation = useValidation({
source: rawStartAddress,
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }],
});
const endIpValidation = useValidation({
source: rawEndAddress,
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }],
});
function onSwitchStartEndClicked() {
const tmpStart = rawStartAddress.value;
rawStartAddress.value = rawEndAddress.value;
rawEndAddress.value = tmpStart;
}
</script>
<style lang="less" scoped></style>

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">
import SpanCopyable from '@/components/SpanCopyable.vue';
import _ from 'lodash';
import SpanCopyable from '@/components/SpanCopyable.vue';
const props = withDefaults(defineProps<{ label: string; oldValue?: string; newValue?: string }>(), {
label: '',
@ -24,4 +12,18 @@ const { label, oldValue, newValue } = toRefs(props);
const testId = computed(() => _.kebabCase(label.value));
</script>
<style scoped lang="less"></style>
<template>
<tr>
<td>
<n-text strong>
{{ label }}
</n-text>
</td>
<td :data-test-id="`${testId}.old`">
<SpanCopyable :value="oldValue" class="monospace" />
</td>
<td :data-test-id="`${testId}.new`">
<SpanCopyable :value="newValue" />
</td>
</tr>
</template>

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">
import { computed } from 'vue';
import { Netmask } from 'netmask';
import { withDefaultOnError } from '@/utils/defaults';
import { isNotThrowing } from '@/utils/boolean';
import { useStorage } from '@vueuse/core';
import { ArrowLeft, ArrowRight } from '@vicons/tabler';
import SpanCopyable from '@/components/SpanCopyable.vue';
import { getIPClass } from './ipv4-subnet-calculator.models';
import { withDefaultOnError } from '@/utils/defaults';
import { isNotThrowing } from '@/utils/boolean';
import SpanCopyable from '@/components/SpanCopyable.vue';
const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24');
@ -61,13 +22,13 @@ const ipValidationRules = [
];
const sections: {
label: string;
getValue: (blocks: Netmask) => string | undefined;
undefinedFallback?: string;
label: string
getValue: (blocks: Netmask) => string | undefined
undefinedFallback?: string
}[] = [
{
label: 'Netmask',
getValue: (block) => block.toString(),
getValue: block => block.toString(),
},
{
label: 'Network address',
@ -122,4 +83,45 @@ function switchToBlock({ count = 1 }: { count?: number }) {
}
</script>
<style lang="less" scoped></style>
<template>
<div>
<c-input-text
v-model:value="ip"
label="An IPv4 address with or without mask"
placeholder="The ipv4 address..."
:validation-rules="ipValidationRules"
mb-4
/>
<div v-if="networkInfo">
<n-table>
<tbody>
<tr v-for="{ getValue, label, undefinedFallback } in sections" :key="label">
<td>
<n-text strong>
{{ label }}
</n-text>
</td>
<td>
<SpanCopyable v-if="getValue(networkInfo)" :value="getValue(networkInfo)" />
<n-text v-else depth="3">
{{ undefinedFallback }}
</n-text>
</td>
</tr>
</tbody>
</n-table>
<div mt-3 flex items-center justify-between>
<c-button @click="switchToBlock({ count: -1 })">
<n-icon :component="ArrowLeft" />
Previous block
</c-button>
<c-button @click="switchToBlock({ count: 1 })">
Next block
<n-icon :component="ArrowRight" />
</c-button>
</div>
</div>
</div>
</template>

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">
import { SHA1 } from 'crypto-js';
import InputCopyable from '@/components/InputCopyable.vue';
@ -43,7 +10,7 @@ const calculatedSections = computed(() => {
.toString()
.substring(30);
const ula = 'fd' + hex40bit.substring(0, 2) + ':' + hex40bit.substring(2, 6) + ':' + hex40bit.substring(6);
const ula = `fd${hex40bit.substring(0, 2)}:${hex40bit.substring(2, 6)}:${hex40bit.substring(6)}`;
return [
{
@ -64,4 +31,35 @@ const calculatedSections = computed(() => {
const addressValidation = macAddressValidation(macAddress);
</script>
<style lang="less" scoped></style>
<template>
<div>
<n-alert title="Info" type="info">
This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed,
and the lower 40 bits to generate your random ULA.
</n-alert>
<c-input-text
v-model:value="macAddress"
placeholder="Type a MAC address"
clearable
label="MAC address:"
raw-text
my-8
:validation="addressValidation"
/>
<div v-if="addressValidation.isValid">
<InputCopyable
v-for="{ label, value } in calculatedSections"
:key="label"
:value="value"
:label="label"
label-width="160px"
label-align="right"
label-position="left"
readonly
mb-2
/>
</div>
</div>
</template>

View file

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

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>
import { useAppTheme } from '@/ui/theme/themes';
import _ from 'lodash';
import { DiffRootViewer } from './diff-viewer.models';
import { diff } from '../json-diff.models';
import { DiffRootViewer } from './diff-viewer.models';
import { useAppTheme } from '@/ui/theme/themes';
const onlyShowDifferences = ref(false);
const props = defineProps<{ leftJson: unknown; rightJson: unknown }>();
const onlyShowDifferences = ref(false);
const { leftJson, rightJson } = toRefs(props);
const appTheme = useAppTheme();
@ -32,6 +17,23 @@ const jsonAreTheSame = computed(() => _.isEqual(leftJson.value, rightJson.value)
const showResults = computed(() => !_.isUndefined(leftJson.value) && !_.isUndefined(rightJson.value));
</script>
<template>
<div v-if="showResults">
<div flex justify-center>
<n-form-item label="Only show differences" label-placement="left">
<n-switch v-model:value="onlyShowDifferences" />
</n-form-item>
</div>
<c-card data-test-id="diff-result">
<n-text v-if="jsonAreTheSame" depth="3" block text-center italic>
The provided JSONs are the same
</n-text>
<DiffRootViewer v-else :diff="result" />
</c-card>
</div>
</template>
<style lang="less" scoped>
::v-deep(.diffs-viewer) {
color: v-bind('appTheme.text.mutedColor');

View file

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from '@playwright/test';
test.describe('Tool - JSON diff', () => {
test.beforeEach(async ({ page }) => {
@ -24,7 +24,7 @@ test.describe('Tool - JSON diff', () => {
const result = await page.getByTestId('diff-result').innerText();
expect(result).toContain(`{\nfoo: "bar""buz",\nbaz: "qux",\n},`);
expect(result).toContain('{\nfoo: "bar""buz",\nbaz: "qux",\n},');
});
test('Different JSONs have only differences listed when "Only show differences" is checked', async ({ page }) => {
@ -34,6 +34,6 @@ test.describe('Tool - JSON diff', () => {
const result = await page.getByTestId('diff-result').innerText();
expect(result).toContain(`{\nbaz: "qux",\n},`);
expect(result).toContain('{\nbaz: "qux",\n},');
});
});

View file

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

View file

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

View file

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

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>
<c-input-text
v-model:value="rawLeftJson"
@ -23,24 +44,3 @@
<DiffsViewer :left-json="leftJson" :right-json="rightJson" />
</template>
<script setup lang="ts">
import JSON5 from 'json5';
import { withDefaultOnError } from '@/utils/defaults';
import { isNotThrowing } from '@/utils/boolean';
import DiffsViewer from './diff-viewer/diff-viewer.vue';
const rawLeftJson = ref('');
const rawRightJson = ref('');
const leftJson = computed(() => withDefaultOnError(() => JSON5.parse(rawLeftJson.value), undefined));
const rightJson = computed(() => withDefaultOnError(() => JSON5.parse(rawRightJson.value), undefined));
const jsonValidationRules = [
{
validator: (value: string) => value === '' || isNotThrowing(() => JSON5.parse(value)),
message: 'Invalid JSON format',
},
];
</script>

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">
import JSON5 from 'json5';
import type { UseValidationRule } from '@/composable/validation';
import { withDefaultOnError } from '@/utils/defaults';
import JSON5 from 'json5';
const defaultValue = '{\n\t"hello": [\n\t\t"world"\n\t]\n}';
const transformer = (value: string) => withDefaultOnError(() => JSON.stringify(JSON5.parse(value), null, 0), '');
@ -25,3 +13,15 @@ const rules: UseValidationRule<string>[] = [
},
];
</script>
<template>
<format-transformer
input-label="Your raw json"
:input-default="defaultValue"
input-placeholder="Paste your raw json here..."
output-label="Minify version of your JSON"
output-language="json"
:input-validation-rules="rules"
:transformer="transformer"
/>
</template>

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.beforeEach(async ({ page }) => {
@ -14,6 +14,6 @@ test.describe('Tool - json to yaml', () => {
const generatedJson = await page.getByTestId('area-content').innerText();
expect(generatedJson.trim()).toEqual(`foo: bar\nlist:\n - item\n - key: value`.trim());
expect(generatedJson.trim()).toEqual('foo: bar\nlist:\n - item\n - key: value'.trim());
});
});

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">
import { stringify } from 'yaml';
import JSON5 from 'json5';
import type { UseValidationRule } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean';
import { withDefaultOnError } from '@/utils/defaults';
import { stringify } from 'yaml';
import JSON5 from 'json5';
const transformer = (value: string) => withDefaultOnError(() => stringify(JSON5.parse(value)), '');
@ -26,4 +15,13 @@ const rules: UseValidationRule<string>[] = [
];
</script>
<style lang="less" scoped></style>
<template>
<format-transformer
input-label="Your JSON"
input-placeholder="Paste your JSON here..."
output-label="YAML from your JSON"
output-language="yaml"
:input-validation-rules="rules"
:transformer="transformer"
/>
</template>

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>
<div style="flex: 0 0 100%">
<div style="margin: 0 auto; max-width: 600px" flex justify-center gap-3>
@ -28,37 +55,10 @@
/>
</n-form-item>
<n-form-item label="Prettify version of your json">
<textarea-copyable :value="cleanJson" language="json" :follow-height-of="inputElement" />
<TextareaCopyable :value="cleanJson" language="json" :follow-height-of="inputElement" />
</n-form-item>
</template>
<script setup lang="ts">
import TextareaCopyable from '@/components/TextareaCopyable.vue';
import { useValidation } from '@/composable/validation';
import { withDefaultOnError } from '@/utils/defaults';
import { computed, ref } from 'vue';
import JSON5 from 'json5';
import { useStorage } from '@vueuse/core';
import { formatJson } from './json.models';
const inputElement = ref<HTMLElement>();
const rawJson = useStorage('json-prettify:raw-json', '{"hello": "world", "foo": "bar"}');
const indentSize = useStorage('json-prettify:indent-size', 3);
const sortKeys = useStorage('json-prettify:sort-keys', true);
const cleanJson = computed(() => withDefaultOnError(() => formatJson({ rawJson, indentSize, sortKeys }), ''));
const rawJsonValidation = useValidation({
source: rawJson,
rules: [
{
validator: (v) => v === '' || JSON5.parse(v),
message: 'Provided JSON is not valid.',
},
],
});
</script>
<style lang="less" scoped>
.result-card {
position: relative;

View file

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

View file

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

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