mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-24 08:46:15 -04:00
chore(lint): switched to a better lint config
This commit is contained in:
parent
4d2b037dbe
commit
33c9b6643f
178 changed files with 4105 additions and 3371 deletions
|
@ -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%;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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('');
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import { escape, unescape } from 'lodash';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const escapeInput = ref('<title>IT Tool</title>');
|
||||
const escapeOutput = computed(() => escape(escapeInput.value));
|
||||
const { copy: copyEscaped } = useCopy({ source: escapeOutput });
|
||||
|
||||
const unescapeInput = ref('<title>IT Tool</title');
|
||||
const unescapeOutput = computed(() => unescape(unescapeInput.value));
|
||||
const { copy: copyUnescaped } = useCopy({ source: unescapeOutput });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card title="Escape html entities">
|
||||
<n-form-item label="Your string :">
|
||||
|
@ -20,7 +34,9 @@
|
|||
</n-form-item>
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button @click="copyEscaped"> Copy </c-button>
|
||||
<c-button @click="copyEscaped">
|
||||
Copy
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
<c-card title="Unescape html entities">
|
||||
|
@ -44,21 +60,9 @@
|
|||
</n-form-item>
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button @click="copyUnescaped"> Copy </c-button>
|
||||
<c-button @click="copyUnescaped">
|
||||
Copy
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { escape, unescape } from 'lodash';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const escapeInput = ref('<title>IT Tool</title>');
|
||||
const escapeOutput = computed(() => escape(escapeInput.value));
|
||||
const { copy: copyEscaped } = useCopy({ source: escapeOutput });
|
||||
|
||||
const unescapeInput = ref('<title>IT Tool</title');
|
||||
const unescapeOutput = computed(() => unescape(unescapeInput.value));
|
||||
const { copy: copyUnescaped } = useCopy({ source: unescapeOutput });
|
||||
</script>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,12 +1,3 @@
|
|||
<template>
|
||||
<div flex items-center>
|
||||
<template v-for="(item, index) in items">
|
||||
<n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical />
|
||||
<menu-bar-item v-else-if="item.type === 'button'" :key="index" v-bind="item" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Editor } from '@tiptap/vue-3';
|
||||
import {
|
||||
|
@ -27,7 +18,7 @@ import {
|
|||
Strikethrough,
|
||||
TextWrap,
|
||||
} from '@vicons/tabler';
|
||||
import { toRefs, type Component } from 'vue';
|
||||
import { type Component, toRefs } from 'vue';
|
||||
import MenuBarItem from './menu-bar-item.vue';
|
||||
|
||||
const props = defineProps<{ editor: Editor }>();
|
||||
|
@ -35,12 +26,12 @@ const { editor } = toRefs(props);
|
|||
|
||||
type MenuItem =
|
||||
| {
|
||||
icon: Component;
|
||||
title: string;
|
||||
action: () => void;
|
||||
isActive?: () => boolean;
|
||||
type: 'button';
|
||||
}
|
||||
icon: Component
|
||||
title: string
|
||||
action: () => void
|
||||
isActive?: () => boolean
|
||||
type: 'button'
|
||||
}
|
||||
| { type: 'divider' };
|
||||
|
||||
const items: MenuItem[] = [
|
||||
|
@ -166,4 +157,11 @@ const items: MenuItem[] = [
|
|||
];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<template>
|
||||
<div flex items-center>
|
||||
<template v-for="(item, index) in items">
|
||||
<n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical />
|
||||
<MenuBarItem v-else-if="item.type === 'button'" :key="index" v-bind="item" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 })),
|
||||
);
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,3 +1,61 @@
|
|||
<script setup lang="ts">
|
||||
import { Exchange } from '@vicons/tabler';
|
||||
import { isValidIpv4 } from '../ipv4-address-converter/ipv4-address-converter.service';
|
||||
import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types';
|
||||
import { calculateCidr } from './ipv4-range-expander.service';
|
||||
import ResultRow from './result-row.vue';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
|
||||
const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1');
|
||||
const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255');
|
||||
|
||||
const result = computed(() => calculateCidr({ startIp: rawStartAddress.value, endIp: rawEndAddress.value }));
|
||||
|
||||
const calculatedValues: {
|
||||
label: string
|
||||
getOldValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined
|
||||
getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined
|
||||
}[] = [
|
||||
{
|
||||
label: 'Start address',
|
||||
getOldValue: () => rawStartAddress.value,
|
||||
getNewValue: result => result?.newStart,
|
||||
},
|
||||
{
|
||||
label: 'End address',
|
||||
getOldValue: () => rawEndAddress.value,
|
||||
getNewValue: result => result?.newEnd,
|
||||
},
|
||||
{
|
||||
label: 'Addresses in range',
|
||||
getOldValue: result => result?.oldSize?.toLocaleString(),
|
||||
getNewValue: result => result?.newSize?.toLocaleString(),
|
||||
},
|
||||
{
|
||||
label: 'CIDR',
|
||||
getOldValue: () => '',
|
||||
getNewValue: result => result?.newCidr,
|
||||
},
|
||||
];
|
||||
|
||||
const startIpValidation = useValidation({
|
||||
source: rawStartAddress,
|
||||
rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }],
|
||||
});
|
||||
const endIpValidation = useValidation({
|
||||
source: rawEndAddress,
|
||||
rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }],
|
||||
});
|
||||
|
||||
const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined);
|
||||
|
||||
function onSwitchStartEndClicked() {
|
||||
const tmpStart = rawStartAddress.value;
|
||||
rawStartAddress.value = rawEndAddress.value;
|
||||
rawEndAddress.value = tmpStart;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div mb-4 flex gap-4>
|
||||
|
@ -21,13 +79,19 @@
|
|||
<n-table v-if="showResult" data-test-id="result">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"> </th>
|
||||
<th scope="col">old value</th>
|
||||
<th scope="col">new value</th>
|
||||
<th scope="col">
|
||||
|
||||
</th>
|
||||
<th scope="col">
|
||||
old value
|
||||
</th>
|
||||
<th scope="col">
|
||||
new value
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<result-row
|
||||
<ResultRow
|
||||
v-for="{ label, getOldValue, getNewValue } in calculatedValues"
|
||||
:key="label"
|
||||
:label="label"
|
||||
|
@ -53,62 +117,3 @@
|
|||
</n-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { Exchange } from '@vicons/tabler';
|
||||
import { isValidIpv4 } from '../ipv4-address-converter/ipv4-address-converter.service';
|
||||
import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types';
|
||||
import { calculateCidr } from './ipv4-range-expander.service';
|
||||
import ResultRow from './result-row.vue';
|
||||
|
||||
const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1');
|
||||
const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255');
|
||||
|
||||
const result = computed(() => calculateCidr({ startIp: rawStartAddress.value, endIp: rawEndAddress.value }));
|
||||
|
||||
const calculatedValues: {
|
||||
label: string;
|
||||
getOldValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined;
|
||||
getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined;
|
||||
}[] = [
|
||||
{
|
||||
label: 'Start address',
|
||||
getOldValue: () => rawStartAddress.value,
|
||||
getNewValue: (result) => result?.newStart,
|
||||
},
|
||||
{
|
||||
label: 'End address',
|
||||
getOldValue: () => rawEndAddress.value,
|
||||
getNewValue: (result) => result?.newEnd,
|
||||
},
|
||||
{
|
||||
label: 'Addresses in range',
|
||||
getOldValue: (result) => result?.oldSize?.toLocaleString(),
|
||||
getNewValue: (result) => result?.newSize?.toLocaleString(),
|
||||
},
|
||||
{
|
||||
label: 'CIDR',
|
||||
getOldValue: () => '',
|
||||
getNewValue: (result) => result?.newCidr,
|
||||
},
|
||||
];
|
||||
|
||||
const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined);
|
||||
const startIpValidation = useValidation({
|
||||
source: rawStartAddress,
|
||||
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }],
|
||||
});
|
||||
const endIpValidation = useValidation({
|
||||
source: rawEndAddress,
|
||||
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }],
|
||||
});
|
||||
|
||||
function onSwitchStartEndClicked() {
|
||||
const tmpStart = rawStartAddress.value;
|
||||
rawStartAddress.value = rawEndAddress.value;
|
||||
rawEndAddress.value = tmpStart;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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},');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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' {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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()}`;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,35 +1,9 @@
|
|||
<template>
|
||||
<c-card>
|
||||
<n-form-item label="JWT to decode" :feedback="validation.message" :validation-status="validation.status">
|
||||
<n-input v-model:value="rawJwt" type="textarea" placeholder="Put your token here..." rows="5" />
|
||||
</n-form-item>
|
||||
|
||||
<n-table v-if="validation.isValid">
|
||||
<tbody>
|
||||
<template v-for="section of sections" :key="section.key">
|
||||
<th colspan="2" class="table-header">{{ section.title }}</th>
|
||||
<tr v-for="{ claim, claimDescription, friendlyValue, value } in decodedJWT[section.key]" :key="claim + value">
|
||||
<td class="claims">
|
||||
<n-text strong>{{ claim }}</n-text>
|
||||
<n-text v-if="claimDescription" depth="3" ml-2>({{ claimDescription }})</n-text>
|
||||
</td>
|
||||
<td>
|
||||
<n-text>{{ value }}</n-text>
|
||||
<n-text v-if="friendlyValue" ml-2 depth="3">({{ friendlyValue }})</n-text>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { decodeJwt } from './jwt-parser.service';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { isNotThrowing } from '@/utils/boolean';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { computed, ref } from 'vue';
|
||||
import { decodeJwt } from './jwt-parser.service';
|
||||
|
||||
const rawJwt = ref(
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
|
||||
|
@ -48,13 +22,47 @@ const validation = useValidation({
|
|||
source: rawJwt,
|
||||
rules: [
|
||||
{
|
||||
validator: (value) => value.length > 0 && isNotThrowing(() => decodeJwt({ jwt: rawJwt.value })),
|
||||
validator: value => value.length > 0 && isNotThrowing(() => decodeJwt({ jwt: rawJwt.value })),
|
||||
message: 'Invalid JWT',
|
||||
},
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<n-form-item label="JWT to decode" :feedback="validation.message" :validation-status="validation.status">
|
||||
<n-input v-model:value="rawJwt" type="textarea" placeholder="Put your token here..." rows="5" />
|
||||
</n-form-item>
|
||||
|
||||
<n-table v-if="validation.isValid">
|
||||
<tbody>
|
||||
<template v-for="section of sections" :key="section.key">
|
||||
<th colspan="2" class="table-header">
|
||||
{{ section.title }}
|
||||
</th>
|
||||
<tr v-for="{ claim, claimDescription, friendlyValue, value } in decodedJWT[section.key]" :key="claim + value">
|
||||
<td class="claims">
|
||||
<n-text strong>
|
||||
{{ claim }}
|
||||
</n-text>
|
||||
<n-text v-if="claimDescription" depth="3" ml-2>
|
||||
({{ claimDescription }})
|
||||
</n-text>
|
||||
</td>
|
||||
<td>
|
||||
<n-text>{{ value }}</n-text>
|
||||
<n-text v-if="friendlyValue" ml-2 depth="3">
|
||||
({{ friendlyValue }})
|
||||
</n-text>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.table-header {
|
||||
text-align: center;
|
||||
|
|
|
@ -1,17 +1,3 @@
|
|||
<template>
|
||||
<div>
|
||||
<c-card style="text-align: center; padding: 40px 0; margin-bottom: 26px">
|
||||
<n-h2 v-if="event">{{ event.key }}</n-h2>
|
||||
<n-text strong depth="3">Press the key on your keyboard you want to get info about this key</n-text>
|
||||
</c-card>
|
||||
|
||||
<n-input-group v-for="({ label, value, placeholder }, i) of fields" :key="i" style="margin-bottom: 5px">
|
||||
<n-input-group-label style="flex: 0 0 150px"> {{ label }} </n-input-group-label>
|
||||
<input-copyable :value="value" readonly :placeholder="placeholder" />
|
||||
</n-input-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
|
@ -24,7 +10,9 @@ useEventListener(document, 'keydown', (e) => {
|
|||
});
|
||||
|
||||
const fields = computed(() => {
|
||||
if (!event.value) return [];
|
||||
if (!event.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
|
@ -64,4 +52,22 @@ const fields = computed(() => {
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<template>
|
||||
<div>
|
||||
<c-card style="text-align: center; padding: 40px 0; margin-bottom: 26px">
|
||||
<n-h2 v-if="event">
|
||||
{{ event.key }}
|
||||
</n-h2>
|
||||
<n-text strong depth="3">
|
||||
Press the key on your keyboard you want to get info about this key
|
||||
</n-text>
|
||||
</c-card>
|
||||
|
||||
<n-input-group v-for="({ label, value, placeholder }, i) of fields" :key="i" style="margin-bottom: 5px">
|
||||
<n-input-group-label style="flex: 0 0 150px">
|
||||
{{ label }}
|
||||
</n-input-group-label>
|
||||
<InputCopyable :value="value" readonly :placeholder="placeholder" />
|
||||
</n-input-group>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Tool - List converter', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
@ -30,10 +30,10 @@ test.describe('Tool - List converter', () => {
|
|||
3
|
||||
5`);
|
||||
await page.getByTestId('removeDuplicates').check();
|
||||
await page.getByTestId('itemPrefix').fill("'");
|
||||
await page.getByTestId('itemSuffix').fill("'");
|
||||
await page.getByTestId('itemPrefix').fill('\'');
|
||||
await page.getByTestId('itemSuffix').fill('\'');
|
||||
|
||||
const result = await page.getByTestId('area-content').innerText();
|
||||
expect(result.trim()).toEqual("'1', '2', '4', '3', '5'");
|
||||
expect(result.trim()).toEqual('\'1\', \'2\', \'4\', \'3\', \'5\'');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, describe, it } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { convert } from './list-converter.models';
|
||||
import type { ConvertOptions } from './list-converter.types';
|
||||
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import _ from 'lodash';
|
||||
import { byOrder } from '@/utils/array';
|
||||
import type { ConvertOptions } from './list-converter.types';
|
||||
import { byOrder } from '@/utils/array';
|
||||
|
||||
export { convert };
|
||||
|
||||
const whenever =
|
||||
<T, R>(condition: boolean, fn: (value: T) => R) =>
|
||||
(value: T) =>
|
||||
function whenever<T, R>(condition: boolean, fn: (value: T) => R) {
|
||||
return (value: T) =>
|
||||
condition ? fn(value) : value;
|
||||
}
|
||||
|
||||
function convert(list: string, options: ConvertOptions): string {
|
||||
const lineBreak = options.keepLineBreaks ? '\n' : '';
|
||||
|
||||
return _.chain(list)
|
||||
.thru(whenever(options.lowerCase, (text) => text.toLowerCase()))
|
||||
.thru(whenever(options.lowerCase, text => text.toLowerCase()))
|
||||
.split('\n')
|
||||
.thru(whenever(options.removeDuplicates, _.uniq))
|
||||
.thru(whenever(options.reverseList, _.reverse))
|
||||
.thru(whenever(!_.isNull(options.sortList), (parts) => parts.sort(byOrder({ order: options.sortList }))))
|
||||
.thru(whenever(!_.isNull(options.sortList), parts => parts.sort(byOrder({ order: options.sortList }))))
|
||||
.map(whenever(options.trimItems, _.trim))
|
||||
.without('')
|
||||
.map((p) => options.itemPrefix + p + options.itemSuffix)
|
||||
.map(p => options.itemPrefix + p + options.itemSuffix)
|
||||
.join(options.separator + lineBreak)
|
||||
.thru((text) => [options.listPrefix, text, options.listSuffix].join(lineBreak))
|
||||
.thru(text => [options.listPrefix, text, options.listSuffix].join(lineBreak))
|
||||
.value();
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
export type SortOrder = 'asc' | 'desc' | null;
|
||||
|
||||
export type ConvertOptions = {
|
||||
lowerCase: boolean;
|
||||
trimItems: boolean;
|
||||
itemPrefix: string;
|
||||
itemSuffix: string;
|
||||
listPrefix: string;
|
||||
listSuffix: string;
|
||||
reverseList: boolean;
|
||||
sortList: SortOrder;
|
||||
removeDuplicates: boolean;
|
||||
separator: string;
|
||||
keepLineBreaks: boolean;
|
||||
};
|
||||
export interface ConvertOptions {
|
||||
lowerCase: boolean
|
||||
trimItems: boolean
|
||||
itemPrefix: string
|
||||
itemSuffix: string
|
||||
listPrefix: string
|
||||
listSuffix: string
|
||||
reverseList: boolean
|
||||
sortList: SortOrder
|
||||
removeDuplicates: boolean
|
||||
separator: string
|
||||
keepLineBreaks: boolean
|
||||
}
|
||||
|
|
|
@ -1,3 +1,40 @@
|
|||
<script setup lang="ts">
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { convert } from './list-converter.models';
|
||||
import type { ConvertOptions } from './list-converter.types';
|
||||
|
||||
const sortOrderOptions = [
|
||||
{
|
||||
label: 'Sort ascending',
|
||||
value: 'asc',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: 'Sort descending',
|
||||
value: 'desc',
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const conversionConfig = useStorage<ConvertOptions>('list-converter:conversionConfig', {
|
||||
lowerCase: false,
|
||||
trimItems: true,
|
||||
removeDuplicates: true,
|
||||
keepLineBreaks: false,
|
||||
itemPrefix: '',
|
||||
itemSuffix: '',
|
||||
listPrefix: '',
|
||||
listSuffix: '',
|
||||
reverseList: false,
|
||||
sortList: null,
|
||||
separator: ', ',
|
||||
});
|
||||
|
||||
function transformer(value: string) {
|
||||
return convert(value, conversionConfig.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="flex: 0 0 100%">
|
||||
<div style="margin: 0 auto; max-width: 600px">
|
||||
|
@ -82,42 +119,3 @@
|
|||
:transformer="transformer"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { convert } from './list-converter.models';
|
||||
import type { ConvertOptions } from './list-converter.types';
|
||||
|
||||
const sortOrderOptions = [
|
||||
{
|
||||
label: 'Sort ascending',
|
||||
value: 'asc',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: 'Sort descending',
|
||||
value: 'desc',
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const conversionConfig = useStorage<ConvertOptions>('list-converter:conversionConfig', {
|
||||
lowerCase: false,
|
||||
trimItems: true,
|
||||
removeDuplicates: true,
|
||||
keepLineBreaks: false,
|
||||
itemPrefix: '',
|
||||
itemSuffix: '',
|
||||
listPrefix: '',
|
||||
listSuffix: '',
|
||||
reverseList: false,
|
||||
sortList: null,
|
||||
separator: ', ',
|
||||
});
|
||||
|
||||
const transformer = (value: string) => {
|
||||
return convert(value, conversionConfig.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -179,13 +179,13 @@ const vocabulary = [
|
|||
];
|
||||
const firstSentence = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
|
||||
|
||||
const generateSentence = (length: number) => {
|
||||
function generateSentence(length: number) {
|
||||
const sentence = Array.from({ length })
|
||||
.map(() => randFromArray(vocabulary))
|
||||
.join(' ');
|
||||
|
||||
return sentence.charAt(0).toUpperCase() + sentence.slice(1) + '.';
|
||||
};
|
||||
return `${sentence.charAt(0).toUpperCase() + sentence.slice(1)}.`;
|
||||
}
|
||||
|
||||
export function generateLoremIpsum({
|
||||
paragraphCount = 1,
|
||||
|
@ -194,11 +194,11 @@ export function generateLoremIpsum({
|
|||
startWithLoremIpsum = true,
|
||||
asHTML = false,
|
||||
}: {
|
||||
paragraphCount?: number;
|
||||
sentencePerParagraph?: number;
|
||||
wordCount?: number;
|
||||
startWithLoremIpsum?: boolean;
|
||||
asHTML?: boolean;
|
||||
paragraphCount?: number
|
||||
sentencePerParagraph?: number
|
||||
wordCount?: number
|
||||
startWithLoremIpsum?: boolean
|
||||
asHTML?: boolean
|
||||
}) {
|
||||
const paragraphs = Array.from({ length: paragraphCount }).map(() =>
|
||||
Array.from({ length: sentencePerParagraph }).map(() => generateSentence(wordCount)),
|
||||
|
@ -209,8 +209,8 @@ export function generateLoremIpsum({
|
|||
}
|
||||
|
||||
if (asHTML) {
|
||||
return `<p>${paragraphs.map((s) => s.join(' ')).join('</p>\n\n<p>')}</p>`;
|
||||
return `<p>${paragraphs.map(s => s.join(' ')).join('</p>\n\n<p>')}</p>`;
|
||||
}
|
||||
|
||||
return paragraphs.map((s) => s.join(' ')).join('\n\n');
|
||||
return paragraphs.map(s => s.join(' ')).join('\n\n');
|
||||
}
|
||||
|
|
|
@ -1,3 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { generateLoremIpsum } from './lorem-ipsum-generator.service';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { randIntFromInterval } from '@/utils/random';
|
||||
|
||||
const paragraphs = ref(1);
|
||||
const sentences = ref([3, 8]);
|
||||
const words = ref([8, 15]);
|
||||
const startWithLoremIpsum = ref(true);
|
||||
const asHTML = ref(false);
|
||||
|
||||
const loremIpsumText = computed(() =>
|
||||
generateLoremIpsum({
|
||||
paragraphCount: paragraphs.value,
|
||||
asHTML: asHTML.value,
|
||||
sentencePerParagraph: randIntFromInterval(sentences.value[0], sentences.value[1]),
|
||||
wordCount: randIntFromInterval(words.value[0], words.value[1]),
|
||||
startWithLoremIpsum: startWithLoremIpsum.value,
|
||||
}),
|
||||
);
|
||||
const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<n-form-item label="Paragraphs" :show-feedback="false" label-width="200" label-placement="left">
|
||||
|
@ -19,31 +43,9 @@
|
|||
<n-input :value="loremIpsumText" type="textarea" placeholder="Your lorem ipsum..." readonly autosize mt-5 />
|
||||
|
||||
<div mt-5 flex justify-center>
|
||||
<c-button autofocus @click="copy"> Copy </c-button>
|
||||
<c-button autofocus @click="copy">
|
||||
Copy
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { ref, computed } from 'vue';
|
||||
import { randIntFromInterval } from '@/utils/random';
|
||||
import { generateLoremIpsum } from './lorem-ipsum-generator.service';
|
||||
|
||||
const paragraphs = ref(1);
|
||||
const sentences = ref([3, 8]);
|
||||
const words = ref([8, 15]);
|
||||
const startWithLoremIpsum = ref(true);
|
||||
const asHTML = ref(false);
|
||||
|
||||
const loremIpsumText = computed(() =>
|
||||
generateLoremIpsum({
|
||||
paragraphCount: paragraphs.value,
|
||||
asHTML: asHTML.value,
|
||||
sentencePerParagraph: randIntFromInterval(sentences.value[0], sentences.value[1]),
|
||||
wordCount: randIntFromInterval(words.value[0], words.value[1]),
|
||||
startWithLoremIpsum: startWithLoremIpsum.value,
|
||||
}),
|
||||
);
|
||||
const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to the clipboard' });
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import db from 'oui/oui.json';
|
||||
import { macAddressValidationRules } from '@/utils/macAddress';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '').toUpperCase().substring(0, 6);
|
||||
|
||||
const macAddress = ref('20:37:06:12:34:56');
|
||||
const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]);
|
||||
|
||||
const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-input-text
|
||||
|
@ -14,32 +27,25 @@
|
|||
mb-5
|
||||
/>
|
||||
|
||||
<div mb-5px>Vendor info:</div>
|
||||
<div mb-5px>
|
||||
Vendor info:
|
||||
</div>
|
||||
<c-card mb-5>
|
||||
<div v-if="details">
|
||||
<div v-for="(detail, index) of details.split('\n')" :key="index">{{ detail }}</div>
|
||||
<div v-for="(detail, index) of details.split('\n')" :key="index">
|
||||
{{ detail }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else italic op-60>Unknown vendor for this address</div>
|
||||
<div v-else italic op-60>
|
||||
Unknown vendor for this address
|
||||
</div>
|
||||
</c-card>
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button :disabled="!details" @click="copy"> Copy vendor info </c-button>
|
||||
<c-button :disabled="!details" @click="copy">
|
||||
Copy vendor info
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import db from 'oui/oui.json';
|
||||
import { macAddressValidationRules } from '@/utils/macAddress';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '').toUpperCase().substring(0, 6);
|
||||
|
||||
const macAddress = ref('20:37:06:12:34:56');
|
||||
const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]);
|
||||
|
||||
const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { defineTool } from '../tool';
|
|||
export const tool = defineTool({
|
||||
name: 'Math evaluator',
|
||||
path: '/math-evaluator',
|
||||
description: `Evaluate math expression, like a calculator on steroid (you can use function like sqrt, cos, sin, abs, ...)`,
|
||||
description: 'Evaluate math expression, like a calculator on steroid (you can use function like sqrt, cos, sin, abs, ...)',
|
||||
keywords: [
|
||||
'math',
|
||||
'evaluator',
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { evaluate } from 'mathjs';
|
||||
import { computed, ref } from 'vue';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
|
||||
const expression = ref('');
|
||||
|
||||
const result = computed(() => withDefaultOnError(() => evaluate(expression.value) ?? '', ''));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-input
|
||||
|
@ -17,13 +27,3 @@
|
|||
</c-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { evaluate } from 'mathjs';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const expression = ref('');
|
||||
|
||||
const result = computed(() => withDefaultOnError(() => evaluate(expression.value) ?? '', ''));
|
||||
</script>
|
||||
|
|
|
@ -3,25 +3,25 @@ import type { SelectGroupOption, SelectOption } from 'naive-ui';
|
|||
export type { OGSchemaType, OGSchemaTypeElementInput, OGSchemaTypeElementSelect, OGSchemaTypeElementInputMultiple };
|
||||
|
||||
interface OGSchemaTypeElementBase {
|
||||
key: string;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
key: string
|
||||
label: string
|
||||
placeholder: string
|
||||
}
|
||||
|
||||
interface OGSchemaTypeElementInput extends OGSchemaTypeElementBase {
|
||||
type: 'input';
|
||||
type: 'input'
|
||||
}
|
||||
|
||||
interface OGSchemaTypeElementInputMultiple extends OGSchemaTypeElementBase {
|
||||
type: 'input-multiple';
|
||||
type: 'input-multiple'
|
||||
}
|
||||
|
||||
interface OGSchemaTypeElementSelect extends OGSchemaTypeElementBase {
|
||||
type: 'select';
|
||||
options: Array<SelectOption | SelectGroupOption>;
|
||||
type: 'select'
|
||||
options: Array<SelectOption | SelectGroupOption>
|
||||
}
|
||||
|
||||
interface OGSchemaType {
|
||||
name: string;
|
||||
elements: (OGSchemaTypeElementSelect | OGSchemaTypeElementInput | OGSchemaTypeElementInputMultiple)[];
|
||||
name: string
|
||||
elements: (OGSchemaTypeElementSelect | OGSchemaTypeElementInput | OGSchemaTypeElementInputMultiple)[]
|
||||
}
|
||||
|
|
|
@ -1,10 +1,65 @@
|
|||
<script setup lang="ts">
|
||||
import { generateMeta } from '@it-tools/oggen';
|
||||
import _ from 'lodash';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { image, ogSchemas, twitter, website } from './og-schemas';
|
||||
import type { OGSchemaType, OGSchemaTypeElementSelect } from './OGSchemaType.type';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
|
||||
// Since type guards do not work in template
|
||||
|
||||
const metadata = ref<{ type: string; [k: string]: any }>({
|
||||
'type': 'website',
|
||||
'twitter:card': 'summary_large_image',
|
||||
});
|
||||
|
||||
watch(
|
||||
() => ref(metadata.value.type),
|
||||
(_ignored, prevSection) => {
|
||||
const section = ogSchemas[prevSection.value];
|
||||
|
||||
if (!section) {
|
||||
return;
|
||||
}
|
||||
|
||||
section.elements.forEach(({ key }) => {
|
||||
metadata.value[key] = '';
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const sections = computed(() => {
|
||||
const secs: OGSchemaType[] = [website, image, twitter];
|
||||
const additionalSchema = ogSchemas[metadata.value.type];
|
||||
|
||||
if (additionalSchema) {
|
||||
secs.push(additionalSchema);
|
||||
}
|
||||
|
||||
return secs;
|
||||
});
|
||||
|
||||
const metaTags = computed(() => {
|
||||
const twitterMeta = _.chain(metadata.value)
|
||||
.pickBy((_value, k) => k.startsWith('twitter:'))
|
||||
.mapKeys((_value, k) => k.replace(/^twitter:/, ''))
|
||||
.value();
|
||||
|
||||
const otherMeta = _.pickBy(metadata.value, (_value, k) => !k.startsWith('twitter:'));
|
||||
|
||||
return generateMeta({ ...otherMeta, twitter: twitterMeta }, { generateTwitterCompatibleMeta: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="{ name, elements } of sections" :key="name" style="margin-bottom: 15px">
|
||||
<n-form-item :label="name" :show-feedback="false"> </n-form-item>
|
||||
<n-form-item :label="name" :show-feedback="false" />
|
||||
|
||||
<n-input-group v-for="{ key, type, label, placeholder, ...element } of elements" :key="key">
|
||||
<n-input-group-label style="flex: 0 0 110px">{{ label }}</n-input-group-label>
|
||||
<n-input-group-label style="flex: 0 0 110px">
|
||||
{{ label }}
|
||||
</n-input-group-label>
|
||||
<c-input-text v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" clearable />
|
||||
<n-dynamic-input
|
||||
v-else-if="type === 'input-multiple'"
|
||||
|
@ -26,60 +81,11 @@
|
|||
</div>
|
||||
<div>
|
||||
<n-form-item label="Your meta tags">
|
||||
<textarea-copyable :value="metaTags" language="html" />
|
||||
<TextareaCopyable :value="metaTags" language="html" />
|
||||
</n-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { generateMeta } from '@it-tools/oggen';
|
||||
import _ from 'lodash';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { image, ogSchemas, twitter, website } from './og-schemas';
|
||||
import type { OGSchemaType, OGSchemaTypeElementSelect } from './OGSchemaType.type';
|
||||
|
||||
// Since type guards do not work in template
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const metadata = ref<{ type: string; [k: string]: any }>({
|
||||
type: 'website',
|
||||
'twitter:card': 'summary_large_image',
|
||||
});
|
||||
|
||||
watch(
|
||||
() => ref(metadata.value.type),
|
||||
(_ignored, prevSection) => {
|
||||
const section = ogSchemas[prevSection.value];
|
||||
|
||||
if (!section) return;
|
||||
|
||||
section.elements.forEach(({ key }) => {
|
||||
metadata.value[key] = '';
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const sections = computed(() => {
|
||||
const secs: OGSchemaType[] = [website, image, twitter];
|
||||
const additionalSchema = ogSchemas[metadata.value.type];
|
||||
|
||||
if (additionalSchema) secs.push(additionalSchema);
|
||||
|
||||
return secs;
|
||||
});
|
||||
|
||||
const metaTags = computed(() => {
|
||||
const twitterMeta = _.chain(metadata.value)
|
||||
.pickBy((_value, k) => k.startsWith('twitter:'))
|
||||
.mapKeys((_value, k) => k.replace(/^twitter:/, ''))
|
||||
.value();
|
||||
|
||||
const otherMeta = _.pickBy(metadata.value, (_value, k) => !k.startsWith('twitter:'));
|
||||
|
||||
return generateMeta({ ...otherMeta, twitter: twitterMeta }, { generateTwitterCompatibleMeta: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-input-group {
|
||||
margin-bottom: 5px;
|
||||
|
|
|
@ -17,7 +17,7 @@ export const videoMovie: OGSchemaType = {
|
|||
placeholder: 'Name of the director...',
|
||||
},
|
||||
{ type: 'input-multiple', label: 'Writer', key: 'video:writer', placeholder: 'Writers of the movie...' },
|
||||
{ type: 'input', label: 'Duration', key: 'video:duration', placeholder: "The movie's length in seconds..." },
|
||||
{ type: 'input', label: 'Duration', key: 'video:duration', placeholder: 'The movie\'s length in seconds...' },
|
||||
{
|
||||
type: 'input',
|
||||
label: 'Release date',
|
||||
|
|
|
@ -1,7 +1,32 @@
|
|||
<script setup lang="ts">
|
||||
import { types as extensionToMimeType, extensions as mimeTypeToExtension } from 'mime-types';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const mimeInfos = Object.entries(mimeTypeToExtension).map(([mimeType, extensions]) => ({ mimeType, extensions }));
|
||||
|
||||
const mimeToExtensionsOptions = Object.keys(mimeTypeToExtension).map(label => ({ label, value: label }));
|
||||
const selectedMimeType = ref(undefined);
|
||||
|
||||
const extensionsFound = computed(() => (selectedMimeType.value ? mimeTypeToExtension[selectedMimeType.value] : []));
|
||||
|
||||
const extensionToMimeTypeOptions = Object.keys(extensionToMimeType).map((label) => {
|
||||
const extension = `.${label}`;
|
||||
|
||||
return { label: extension, value: label };
|
||||
});
|
||||
const selectedExtension = ref(undefined);
|
||||
|
||||
const mimeTypeFound = computed(() => (selectedExtension.value ? extensionToMimeType[selectedExtension.value] : []));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<n-h2 style="margin-bottom: 0">Mime type to extension</n-h2>
|
||||
<div style="opacity: 0.8">Now witch file extensions are associated to a mime-type</div>
|
||||
<n-h2 style="margin-bottom: 0">
|
||||
Mime type to extension
|
||||
</n-h2>
|
||||
<div style="opacity: 0.8">
|
||||
Now witch file extensions are associated to a mime-type
|
||||
</div>
|
||||
<n-form-item>
|
||||
<n-select
|
||||
v-model:value="selectedMimeType"
|
||||
|
@ -13,7 +38,9 @@
|
|||
</n-form-item>
|
||||
|
||||
<div v-if="extensionsFound.length > 0">
|
||||
Extensions of files with the <n-tag round :bordered="false">{{ selectedMimeType }}</n-tag> mime-type:
|
||||
Extensions of files with the <n-tag round :bordered="false">
|
||||
{{ selectedMimeType }}
|
||||
</n-tag> mime-type:
|
||||
<div style="margin-top: 10px">
|
||||
<n-tag
|
||||
v-for="extension of extensionsFound"
|
||||
|
@ -30,8 +57,12 @@
|
|||
</c-card>
|
||||
|
||||
<c-card>
|
||||
<n-h2 style="margin-bottom: 0">File extension to mime type</n-h2>
|
||||
<div style="opacity: 0.8">Now witch mime type is associated to a file extension</div>
|
||||
<n-h2 style="margin-bottom: 0">
|
||||
File extension to mime type
|
||||
</n-h2>
|
||||
<div style="opacity: 0.8">
|
||||
Now witch mime type is associated to a file extension
|
||||
</div>
|
||||
<n-form-item>
|
||||
<n-select
|
||||
v-model:value="selectedExtension"
|
||||
|
@ -43,7 +74,9 @@
|
|||
</n-form-item>
|
||||
|
||||
<div v-if="selectedExtension">
|
||||
Mime type associated to the extension <n-tag round :bordered="false">{{ selectedExtension }}</n-tag> file
|
||||
Mime type associated to the extension <n-tag round :bordered="false">
|
||||
{{ selectedExtension }}
|
||||
</n-tag> file
|
||||
extension:
|
||||
<div style="margin-top: 10px">
|
||||
<n-tag round :bordered="false" type="primary" style="margin-right: 10px">
|
||||
|
@ -74,24 +107,3 @@
|
|||
</n-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { types as extensionToMimeType, extensions as mimeTypeToExtension } from 'mime-types';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const mimeInfos = Object.entries(mimeTypeToExtension).map(([mimeType, extensions]) => ({ mimeType, extensions }));
|
||||
|
||||
const mimeToExtensionsOptions = Object.keys(mimeTypeToExtension).map((label) => ({ label, value: label }));
|
||||
const selectedMimeType = ref(undefined);
|
||||
|
||||
const extensionsFound = computed(() => (selectedMimeType.value ? mimeTypeToExtension[selectedMimeType.value] : []));
|
||||
|
||||
const extensionToMimeTypeOptions = Object.keys(extensionToMimeType).map((label) => {
|
||||
const extension = `.${label}`;
|
||||
|
||||
return { label: extension, value: label };
|
||||
});
|
||||
const selectedExtension = ref(undefined);
|
||||
|
||||
const mimeTypeFound = computed(() => (selectedExtension.value ? extensionToMimeType[selectedExtension.value] : []));
|
||||
</script>
|
||||
|
|
|
@ -1,86 +1,13 @@
|
|||
<template>
|
||||
<div style="max-width: 350px">
|
||||
<c-input-text
|
||||
v-model:value="secret"
|
||||
label="Secret"
|
||||
placeholder="Paste your TOTP secret..."
|
||||
mb-5
|
||||
:validation-rules="secretValidationRules"
|
||||
>
|
||||
<template #suffix>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button circle variant="text" size="small" @click="refreshSecret">
|
||||
<icon-mdi-refresh />
|
||||
</c-button>
|
||||
</template>
|
||||
Generate secret token
|
||||
</n-tooltip>
|
||||
</template>
|
||||
</c-input-text>
|
||||
|
||||
<div>
|
||||
<token-display :tokens="tokens" style="margin-top: 2px" />
|
||||
|
||||
<n-progress :percentage="(100 * interval) / 30" :color="theme.primaryColor" :show-indicator="false" />
|
||||
<div style="text-align: center">Next in {{ String(Math.floor(30 - interval)).padStart(2, '0') }}s</div>
|
||||
</div>
|
||||
<div mt-4 flex flex-col items-center justify-center gap-3>
|
||||
<n-image :src="qrcode"></n-image>
|
||||
<c-button :href="keyUri" target="_blank">Open Key URI in new tab</c-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width: 350px">
|
||||
<input-copyable
|
||||
label="Secret in hexadecimal"
|
||||
:value="base32toHex(secret)"
|
||||
readonly
|
||||
placeholder="Secret in hex will be displayed here"
|
||||
mb-5
|
||||
/>
|
||||
|
||||
<input-copyable
|
||||
label="Epoch"
|
||||
:value="Math.floor(now / 1000).toString()"
|
||||
readonly
|
||||
mb-5
|
||||
placeholder="Epoch in sec will be displayed here"
|
||||
/>
|
||||
|
||||
<p>Iteration</p>
|
||||
|
||||
<input-copyable
|
||||
:value="String(getCounterFromTime({ now, timeStep: 30 }))"
|
||||
readonly
|
||||
label="Count:"
|
||||
label-position="left"
|
||||
label-width="90px"
|
||||
label-align="right"
|
||||
placeholder="Iteration count will be displayed here"
|
||||
/>
|
||||
|
||||
<input-copyable
|
||||
:value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')"
|
||||
readonly
|
||||
placeholder="Iteration count in hex will be displayed here"
|
||||
label-position="left"
|
||||
label-width="90px"
|
||||
label-align="right"
|
||||
label="Padded hex:"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useTimestamp } from '@vueuse/core';
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import { useQRCode } from '../qr-code-generator/useQRCode';
|
||||
import { base32toHex, buildKeyUri, generateSecret, generateTOTP, getCounterFromTime } from './otp.service';
|
||||
import TokenDisplay from './token-display.vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
import InputCopyable from '@/components/InputCopyable.vue';
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
import { generateTOTP, buildKeyUri, generateSecret, base32toHex, getCounterFromTime } from './otp.service';
|
||||
import { useQRCode } from '../qr-code-generator/useQRCode';
|
||||
import TokenDisplay from './token-display.vue';
|
||||
|
||||
const now = useTimestamp();
|
||||
const interval = computed(() => (now.value / 1000) % 30);
|
||||
|
@ -125,6 +52,83 @@ const secretValidationRules = [
|
|||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="max-width: 350px">
|
||||
<c-input-text
|
||||
v-model:value="secret"
|
||||
label="Secret"
|
||||
placeholder="Paste your TOTP secret..."
|
||||
mb-5
|
||||
:validation-rules="secretValidationRules"
|
||||
>
|
||||
<template #suffix>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<c-button circle variant="text" size="small" @click="refreshSecret">
|
||||
<icon-mdi-refresh />
|
||||
</c-button>
|
||||
</template>
|
||||
Generate secret token
|
||||
</n-tooltip>
|
||||
</template>
|
||||
</c-input-text>
|
||||
|
||||
<div>
|
||||
<TokenDisplay :tokens="tokens" style="margin-top: 2px" />
|
||||
|
||||
<n-progress :percentage="(100 * interval) / 30" :color="theme.primaryColor" :show-indicator="false" />
|
||||
<div style="text-align: center">
|
||||
Next in {{ String(Math.floor(30 - interval)).padStart(2, '0') }}s
|
||||
</div>
|
||||
</div>
|
||||
<div mt-4 flex flex-col items-center justify-center gap-3>
|
||||
<n-image :src="qrcode" />
|
||||
<c-button :href="keyUri" target="_blank">
|
||||
Open Key URI in new tab
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width: 350px">
|
||||
<InputCopyable
|
||||
label="Secret in hexadecimal"
|
||||
:value="base32toHex(secret)"
|
||||
readonly
|
||||
placeholder="Secret in hex will be displayed here"
|
||||
mb-5
|
||||
/>
|
||||
|
||||
<InputCopyable
|
||||
label="Epoch"
|
||||
:value="Math.floor(now / 1000).toString()"
|
||||
readonly
|
||||
mb-5
|
||||
placeholder="Epoch in sec will be displayed here"
|
||||
/>
|
||||
|
||||
<p>Iteration</p>
|
||||
|
||||
<InputCopyable
|
||||
:value="String(getCounterFromTime({ now, timeStep: 30 }))"
|
||||
readonly
|
||||
label="Count:"
|
||||
label-position="left"
|
||||
label-width="90px"
|
||||
label-align="right"
|
||||
placeholder="Iteration count will be displayed here"
|
||||
/>
|
||||
|
||||
<InputCopyable
|
||||
:value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')"
|
||||
readonly
|
||||
placeholder="Iteration count in hex will be displayed here"
|
||||
label-position="left"
|
||||
label-width="90px"
|
||||
label-align="right"
|
||||
label="Padded hex:"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-progress {
|
||||
margin-top: 10px;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Tool - OTP code generator', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
@ -19,7 +19,7 @@ test.describe('Tool - OTP code generator', () => {
|
|||
|
||||
test('OTP a generated from the provided secret', async ({ page }) => {
|
||||
page.evaluate(() => {
|
||||
Date.now = () => 1609477200000; //Jan 1, 2021
|
||||
Date.now = () => 1609477200000; // Jan 1, 2021
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Paste your TOTP secret...').fill('ITTOOLS');
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
base32toHex,
|
||||
buildKeyUri,
|
||||
generateHOTP,
|
||||
generateTOTP,
|
||||
hexToBytes,
|
||||
verifyHOTP,
|
||||
generateTOTP,
|
||||
verifyTOTP,
|
||||
buildKeyUri,
|
||||
base32toHex,
|
||||
} from './otp.service';
|
||||
|
||||
describe('otp functions', () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { enc, HmacSHA1 } from 'crypto-js';
|
||||
import { HmacSHA1, enc } from 'crypto-js';
|
||||
import _ from 'lodash';
|
||||
import { createToken } from '../token-generator/token-generator.service';
|
||||
|
||||
|
@ -15,7 +15,7 @@ export {
|
|||
};
|
||||
|
||||
function hexToBytes(hex: string) {
|
||||
return (hex.match(/.{1,2}/g) ?? []).map((char) => parseInt(char, 16));
|
||||
return (hex.match(/.{1,2}/g) ?? []).map(char => parseInt(char, 16));
|
||||
}
|
||||
|
||||
function computeHMACSha1(message: string, key: string) {
|
||||
|
@ -29,10 +29,10 @@ function base32toHex(base32: string) {
|
|||
.toUpperCase() // Since base 32, we coerce lowercase to uppercase
|
||||
.replace(/=+$/, '')
|
||||
.split('')
|
||||
.map((value) => base32Chars.indexOf(value).toString(2).padStart(5, '0'))
|
||||
.map(value => base32Chars.indexOf(value).toString(2).padStart(5, '0'))
|
||||
.join('');
|
||||
|
||||
const hex = (bits.match(/.{1,8}/g) ?? []).map((chunk) => parseInt(chunk, 2).toString(16).padStart(2, '0')).join('');
|
||||
const hex = (bits.match(/.{1,8}/g) ?? []).map(chunk => parseInt(chunk, 2).toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return hex;
|
||||
}
|
||||
|
@ -45,12 +45,12 @@ function generateHOTP({ key, counter = 0 }: { key: string; counter?: number }) {
|
|||
const bytes = hexToBytes(digest);
|
||||
|
||||
// Truncate
|
||||
const offset = bytes[19] & 0xf;
|
||||
const v =
|
||||
((bytes[offset] & 0x7f) << 24) |
|
||||
((bytes[offset + 1] & 0xff) << 16) |
|
||||
((bytes[offset + 2] & 0xff) << 8) |
|
||||
(bytes[offset + 3] & 0xff);
|
||||
const offset = bytes[19] & 0xF;
|
||||
const v
|
||||
= ((bytes[offset] & 0x7F) << 24)
|
||||
| ((bytes[offset + 1] & 0xFF) << 16)
|
||||
| ((bytes[offset + 2] & 0xFF) << 8)
|
||||
| (bytes[offset + 3] & 0xFF);
|
||||
|
||||
const code = String(v % 1000000).padStart(6, '0');
|
||||
|
||||
|
@ -63,10 +63,10 @@ function verifyHOTP({
|
|||
window = 0,
|
||||
counter = 0,
|
||||
}: {
|
||||
token: string;
|
||||
key: string;
|
||||
window?: number;
|
||||
counter?: number;
|
||||
token: string
|
||||
key: string
|
||||
window?: number
|
||||
counter?: number
|
||||
}) {
|
||||
for (let i = counter - window; i <= counter + window; ++i) {
|
||||
if (generateHOTP({ key, counter: i }) === token) {
|
||||
|
@ -94,11 +94,11 @@ function verifyTOTP({
|
|||
now = Date.now(),
|
||||
timeStep = 30,
|
||||
}: {
|
||||
token: string;
|
||||
key: string;
|
||||
window?: number;
|
||||
now?: number;
|
||||
timeStep?: number;
|
||||
token: string
|
||||
key: string
|
||||
window?: number
|
||||
now?: number
|
||||
timeStep?: number
|
||||
}) {
|
||||
const counter = getCounterFromTime({ now, timeStep });
|
||||
|
||||
|
@ -113,12 +113,12 @@ function buildKeyUri({
|
|||
digits = 6,
|
||||
period = 30,
|
||||
}: {
|
||||
secret: string;
|
||||
app?: string;
|
||||
account?: string;
|
||||
algorithm?: string;
|
||||
digits?: number;
|
||||
period?: number;
|
||||
secret: string
|
||||
app?: string
|
||||
account?: string
|
||||
algorithm?: string
|
||||
digits?: number
|
||||
period?: number
|
||||
}) {
|
||||
const params = {
|
||||
issuer: app,
|
||||
|
|
|
@ -1,9 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { toRefs } from 'vue';
|
||||
|
||||
const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>();
|
||||
const { copy: copyPrevious, copied: previousCopied } = useClipboard();
|
||||
const { copy: copyCurrent, copied: currentCopied } = useClipboard();
|
||||
const { copy: copyNext, copied: nextCopied } = useClipboard();
|
||||
|
||||
const { tokens } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="labels" w-full flex items-center>
|
||||
<div flex-1 text-left>Previous</div>
|
||||
<div flex-1 text-center>Current OTP</div>
|
||||
<div flex-1 text-right>Next</div>
|
||||
<div flex-1 text-left>
|
||||
Previous
|
||||
</div>
|
||||
<div flex-1 text-center>
|
||||
Current OTP
|
||||
</div>
|
||||
<div flex-1 text-right>
|
||||
Next
|
||||
</div>
|
||||
</div>
|
||||
<n-input-group>
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
|
@ -29,9 +47,11 @@
|
|||
</n-tooltip>
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<c-button important:h-12 data-test-id="next-otp" @click.prevent="copyNext(tokens.next)">{{
|
||||
tokens.next
|
||||
}}</c-button>
|
||||
<c-button important:h-12 data-test-id="next-otp" @click.prevent="copyNext(tokens.next)">
|
||||
{{
|
||||
tokens.next
|
||||
}}
|
||||
</c-button>
|
||||
</template>
|
||||
<div>{{ nextCopied ? 'Copied !' : 'Copy next OTP' }}</div>
|
||||
</n-tooltip>
|
||||
|
@ -39,18 +59,6 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { toRefs } from 'vue';
|
||||
|
||||
const { copy: copyPrevious, copied: previousCopied } = useClipboard();
|
||||
const { copy: copyCurrent, copied: currentCopied } = useClipboard();
|
||||
const { copy: copyNext, copied: nextCopied } = useClipboard();
|
||||
|
||||
const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>();
|
||||
const { tokens } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.current-otp {
|
||||
font-size: 22px;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Tool - Phone parser and formatter', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
|
|
@ -18,13 +18,17 @@ const typeToLabel: Record<NonNullable<NumberType>, string> = {
|
|||
};
|
||||
|
||||
function formatTypeToHumanReadable(type: NumberType): string | undefined {
|
||||
if (!type) return undefined;
|
||||
if (!type) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return typeToLabel[type];
|
||||
}
|
||||
|
||||
function getFullCountryName(countryCode: string | undefined) {
|
||||
if (!countryCode) return undefined;
|
||||
if (!countryCode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return lookup.byIso(countryCode)?.country;
|
||||
}
|
||||
|
@ -35,7 +39,9 @@ function getDefaultCountryCode({
|
|||
}: { locale?: string; defaultCode?: CountryCode } = {}): CountryCode {
|
||||
const countryCode = locale.split('-')[1]?.toUpperCase();
|
||||
|
||||
if (!countryCode) return defaultCode;
|
||||
if (!countryCode) {
|
||||
return defaultCode;
|
||||
}
|
||||
|
||||
return (lookup.byIso(countryCode)?.iso2 ?? defaultCode) as CountryCode;
|
||||
}
|
||||
|
|
|
@ -1,44 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<n-form-item label="Default country code:">
|
||||
<n-select v-model:value="defaultCountryCode" :options="countriesOptions" />
|
||||
</n-form-item>
|
||||
|
||||
<c-input-text
|
||||
v-model:value="rawPhone"
|
||||
placeholder="Enter a phone number"
|
||||
label="Phone number:"
|
||||
:validation="validation"
|
||||
mb-5
|
||||
/>
|
||||
|
||||
<n-table v-if="parsedDetails">
|
||||
<tbody>
|
||||
<tr v-for="{ label, value } in parsedDetails" :key="label">
|
||||
<td>
|
||||
<n-text strong>{{ label }}</n-text>
|
||||
</td>
|
||||
<td>
|
||||
<span-copyable v-if="value" :value="value"></span-copyable>
|
||||
<n-text v-else depth="3" italic>Unknown</n-text>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { parsePhoneNumber, getCountries, getCountryCallingCode } from 'libphonenumber-js/max';
|
||||
import { booleanToHumanReadable } from '@/utils/boolean';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { getCountries, getCountryCallingCode, parsePhoneNumber } from 'libphonenumber-js/max';
|
||||
import lookup from 'country-code-lookup';
|
||||
import {
|
||||
formatTypeToHumanReadable,
|
||||
getFullCountryName,
|
||||
getDefaultCountryCode,
|
||||
getFullCountryName,
|
||||
} from './phone-parser-and-formatter.models';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { booleanToHumanReadable } from '@/utils/boolean';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
|
||||
const rawPhone = ref('');
|
||||
const defaultCountryCode = ref(getDefaultCountryCode());
|
||||
|
@ -46,18 +16,22 @@ const validation = useValidation({
|
|||
source: rawPhone,
|
||||
rules: [
|
||||
{
|
||||
validator: (value) => value === '' || /^[0-9 +\-()]+$/.test(value),
|
||||
validator: value => value === '' || /^[0-9 +\-()]+$/.test(value),
|
||||
message: 'Invalid phone number',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const parsedDetails = computed(() => {
|
||||
if (!validation.isValid) return undefined;
|
||||
if (!validation.isValid) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = withDefaultOnError(() => parsePhoneNumber(rawPhone.value, defaultCountryCode.value), undefined);
|
||||
|
||||
if (!parsed) return undefined;
|
||||
if (!parsed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
|
@ -103,10 +77,42 @@ const parsedDetails = computed(() => {
|
|||
];
|
||||
});
|
||||
|
||||
const countriesOptions = getCountries().map((code) => ({
|
||||
const countriesOptions = getCountries().map(code => ({
|
||||
label: `${lookup.byIso(code)?.country || code} (+${getCountryCallingCode(code)})`,
|
||||
value: code,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<template>
|
||||
<div>
|
||||
<n-form-item label="Default country code:">
|
||||
<n-select v-model:value="defaultCountryCode" :options="countriesOptions" />
|
||||
</n-form-item>
|
||||
|
||||
<c-input-text
|
||||
v-model:value="rawPhone"
|
||||
placeholder="Enter a phone number"
|
||||
label="Phone number:"
|
||||
:validation="validation"
|
||||
mb-5
|
||||
/>
|
||||
|
||||
<n-table v-if="parsedDetails">
|
||||
<tbody>
|
||||
<tr v-for="{ label, value } in parsedDetails" :key="label">
|
||||
<td>
|
||||
<n-text strong>
|
||||
{{ label }}
|
||||
</n-text>
|
||||
</td>
|
||||
<td>
|
||||
<span-copyable v-if="value" :value="value" />
|
||||
<n-text v-else depth="3" italic>
|
||||
Unknown
|
||||
</n-text>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,3 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import type { QRCodeErrorCorrectionLevel } from 'qrcode';
|
||||
import { useQRCode } from './useQRCode';
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
|
||||
const foreground = ref('#000000ff');
|
||||
const background = ref('#ffffffff');
|
||||
const errorCorrectionLevel = ref<QRCodeErrorCorrectionLevel>('medium');
|
||||
|
||||
const errorCorrectionLevels = ['low', 'medium', 'quartile', 'high'];
|
||||
|
||||
const text = ref('https://it-tools.tech');
|
||||
const { qrcode } = useQRCode({
|
||||
text,
|
||||
color: {
|
||||
background,
|
||||
foreground,
|
||||
},
|
||||
errorCorrectionLevel,
|
||||
options: { width: 1024 },
|
||||
});
|
||||
|
||||
const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-code.png' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<n-grid x-gap="12" y-gap="12" cols="1 600:3">
|
||||
|
@ -28,35 +54,11 @@
|
|||
<n-gi>
|
||||
<div flex flex-col items-center gap-3>
|
||||
<n-image :src="qrcode" width="200" />
|
||||
<c-button @click="download"> Download qr-code </c-button>
|
||||
<c-button @click="download">
|
||||
Download qr-code
|
||||
</c-button>
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
import { ref } from 'vue';
|
||||
import type { QRCodeErrorCorrectionLevel } from 'qrcode';
|
||||
import { useQRCode } from './useQRCode';
|
||||
|
||||
const foreground = ref('#000000ff');
|
||||
const background = ref('#ffffffff');
|
||||
const errorCorrectionLevel = ref<QRCodeErrorCorrectionLevel>('medium');
|
||||
|
||||
const errorCorrectionLevels = ['low', 'medium', 'quartile', 'high'];
|
||||
|
||||
const text = ref('https://it-tools.tech');
|
||||
const { qrcode } = useQRCode({
|
||||
text,
|
||||
color: {
|
||||
background,
|
||||
foreground,
|
||||
},
|
||||
errorCorrectionLevel,
|
||||
options: { width: 1024 },
|
||||
});
|
||||
|
||||
const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-code.png' });
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { get, type MaybeRef } from '@vueuse/core';
|
||||
import { type MaybeRef, get } from '@vueuse/core';
|
||||
import QRCode, { type QRCodeErrorCorrectionLevel, type QRCodeToDataURLOptions } from 'qrcode';
|
||||
import { ref, watch, isRef } from 'vue';
|
||||
import { isRef, ref, watch } from 'vue';
|
||||
|
||||
export function useQRCode({
|
||||
text,
|
||||
|
@ -8,17 +8,17 @@ export function useQRCode({
|
|||
errorCorrectionLevel,
|
||||
options,
|
||||
}: {
|
||||
text: MaybeRef<string>;
|
||||
color: { foreground: MaybeRef<string>; background: MaybeRef<string> };
|
||||
errorCorrectionLevel?: MaybeRef<QRCodeErrorCorrectionLevel>;
|
||||
options?: QRCodeToDataURLOptions;
|
||||
text: MaybeRef<string>
|
||||
color: { foreground: MaybeRef<string>; background: MaybeRef<string> }
|
||||
errorCorrectionLevel?: MaybeRef<QRCodeErrorCorrectionLevel>
|
||||
options?: QRCodeToDataURLOptions
|
||||
}) {
|
||||
const qrcode = ref('');
|
||||
|
||||
watch(
|
||||
[text, background, foreground, errorCorrectionLevel].filter(isRef),
|
||||
async () => {
|
||||
if (get(text))
|
||||
if (get(text)) {
|
||||
qrcode.value = await QRCode.toDataURL(get(text).trim(), {
|
||||
color: {
|
||||
dark: get(foreground),
|
||||
|
@ -28,6 +28,7 @@ export function useQRCode({
|
|||
errorCorrectionLevel: get(errorCorrectionLevel) ?? 'M',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import { generatePort } from './random-port-generator.model';
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
|
||||
const [port, refreshPort] = computedRefreshable(() => String(generatePort()));
|
||||
|
||||
const { copy } = useCopy({ source: port, text: 'Port copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<div class="port">
|
||||
{{ port }}
|
||||
</div>
|
||||
<div flex justify-center gap-3>
|
||||
<c-button @click="copy"> Copy </c-button>
|
||||
<c-button @click="refreshPort"> Refresh </c-button>
|
||||
<c-button @click="copy">
|
||||
Copy
|
||||
</c-button>
|
||||
<c-button @click="refreshPort">
|
||||
Refresh
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { generatePort } from './random-port-generator.model';
|
||||
|
||||
const [port, refreshPort] = computedRefreshable(() => String(generatePort()));
|
||||
|
||||
const { copy } = useCopy({ source: port, text: 'Port copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.port {
|
||||
text-align: center;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, describe, it } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { arabicToRoman } from './roman-numeral-converter.service';
|
||||
|
||||
describe('roman-numeral-converter', () => {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
export const MIN_ARABIC_TO_ROMAN = 1;
|
||||
export const MAX_ARABIC_TO_ROMAN = 3999;
|
||||
export function arabicToRoman(num: number) {
|
||||
if (num < MIN_ARABIC_TO_ROMAN || num > MAX_ARABIC_TO_ROMAN) return '';
|
||||
if (num < MIN_ARABIC_TO_ROMAN || num > MAX_ARABIC_TO_ROMAN) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lookup: { [key: string]: number } = {
|
||||
M: 1000,
|
||||
|
@ -28,7 +30,7 @@ export function arabicToRoman(num: number) {
|
|||
return roman;
|
||||
}
|
||||
|
||||
const ROMAN_NUMBER_REGEX = new RegExp(/^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/);
|
||||
const ROMAN_NUMBER_REGEX = /^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/;
|
||||
|
||||
export function isValidRomanNumber(romanNumber: string) {
|
||||
return ROMAN_NUMBER_REGEX.test(romanNumber);
|
||||
|
|
|
@ -1,3 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import {
|
||||
MAX_ARABIC_TO_ROMAN,
|
||||
MIN_ARABIC_TO_ROMAN,
|
||||
arabicToRoman,
|
||||
isValidRomanNumber,
|
||||
romanToArabic,
|
||||
} from './roman-numeral-converter.service';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
|
||||
const inputNumeral = ref(42);
|
||||
const outputRoman = computed(() => arabicToRoman(inputNumeral.value));
|
||||
|
||||
const { attrs: validationNumeral } = useValidation({
|
||||
source: inputNumeral,
|
||||
rules: [
|
||||
{
|
||||
validator: value => value >= MIN_ARABIC_TO_ROMAN && value <= MAX_ARABIC_TO_ROMAN,
|
||||
message: `We can only convert numbers between ${MIN_ARABIC_TO_ROMAN.toLocaleString()} and ${MAX_ARABIC_TO_ROMAN.toLocaleString()}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const inputRoman = ref('XLII');
|
||||
const outputNumeral = computed(() => romanToArabic(inputRoman.value));
|
||||
|
||||
const validationRoman = useValidation({
|
||||
source: inputRoman,
|
||||
rules: [
|
||||
{
|
||||
validator: value => isValidRomanNumber(value),
|
||||
message: 'The input you entered is not a valid roman number',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { copy: copyRoman } = useCopy({ source: outputRoman, text: 'Roman number copied to the clipboard' });
|
||||
const { copy: copyArabic } = useCopy({ source: outputNumeral, text: 'Arabic number copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<c-card title="Arabic to roman">
|
||||
|
@ -20,54 +62,14 @@
|
|||
<div class="result">
|
||||
{{ outputNumeral }}
|
||||
</div>
|
||||
<c-button :disabled="!validationRoman.isValid" @click="copyArabic"> Copy </c-button>
|
||||
<c-button :disabled="!validationRoman.isValid" @click="copyArabic">
|
||||
Copy
|
||||
</c-button>
|
||||
</div>
|
||||
</c-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import {
|
||||
arabicToRoman,
|
||||
romanToArabic,
|
||||
MAX_ARABIC_TO_ROMAN,
|
||||
MIN_ARABIC_TO_ROMAN,
|
||||
isValidRomanNumber,
|
||||
} from './roman-numeral-converter.service';
|
||||
|
||||
const inputNumeral = ref(42);
|
||||
const outputRoman = computed(() => arabicToRoman(inputNumeral.value));
|
||||
|
||||
const { attrs: validationNumeral } = useValidation({
|
||||
source: inputNumeral,
|
||||
rules: [
|
||||
{
|
||||
validator: (value) => value >= MIN_ARABIC_TO_ROMAN && value <= MAX_ARABIC_TO_ROMAN,
|
||||
message: `We can only convert numbers between ${MIN_ARABIC_TO_ROMAN.toLocaleString()} and ${MAX_ARABIC_TO_ROMAN.toLocaleString()}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const inputRoman = ref('XLII');
|
||||
const outputNumeral = computed(() => romanToArabic(inputRoman.value));
|
||||
|
||||
const validationRoman = useValidation({
|
||||
source: inputRoman,
|
||||
rules: [
|
||||
{
|
||||
validator: (value) => isValidRomanNumber(value),
|
||||
message: `The input you entered is not a valid roman number`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { copy: copyRoman } = useCopy({ source: outputRoman, text: 'Roman number copied to the clipboard' });
|
||||
const { copy: copyArabic } = useCopy({ source: outputNumeral, text: 'Arabic number copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.result {
|
||||
font-size: 22px;
|
||||
|
|
|
@ -1,32 +1,10 @@
|
|||
<template>
|
||||
<div style="flex: 0 0 100%">
|
||||
<div item-style="flex: 1 1 0" style="max-width: 600px" mx-auto flex gap-3>
|
||||
<n-form-item label="Bits :" v-bind="bitsValidationAttrs as any" label-placement="left" label-width="100">
|
||||
<n-input-number v-model:value="bits" min="256" max="16384" step="8" />
|
||||
</n-form-item>
|
||||
|
||||
<c-button @click="refreshCerts">Refresh key-pair</c-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Public key</h3>
|
||||
<textarea-copyable :value="certs.publicKeyPem" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Private key</h3>
|
||||
<textarea-copyable :value="certs.privateKeyPem" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { ref } from 'vue';
|
||||
import { generateKeyPair } from './rsa-key-pair-generator.service';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { withDefaultOnErrorAsync } from '@/utils/defaults';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { computedRefreshableAsync } from '@/composable/computedRefreshable';
|
||||
import { generateKeyPair } from './rsa-key-pair-generator.service';
|
||||
|
||||
const bits = ref(2048);
|
||||
const emptyCerts = { publicKeyPem: '', privateKeyPem: '' };
|
||||
|
@ -36,7 +14,7 @@ const { attrs: bitsValidationAttrs } = useValidation({
|
|||
rules: [
|
||||
{
|
||||
message: 'Bits should be 256 <= bits <= 16384 and be a multiple of 8',
|
||||
validator: (value) => value >= 256 && value <= 16384 && value % 8 === 0,
|
||||
validator: value => value >= 256 && value <= 16384 && value % 8 === 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -47,4 +25,26 @@ const [certs, refreshCerts] = computedRefreshableAsync(
|
|||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<template>
|
||||
<div style="flex: 0 0 100%">
|
||||
<div item-style="flex: 1 1 0" style="max-width: 600px" mx-auto flex gap-3>
|
||||
<n-form-item label="Bits :" v-bind="bitsValidationAttrs as any" label-placement="left" label-width="100">
|
||||
<n-input-number v-model:value="bits" min="256" max="16384" step="8" />
|
||||
</n-form-item>
|
||||
|
||||
<c-button @click="refreshCerts">
|
||||
Refresh key-pair
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Public key</h3>
|
||||
<TextareaCopyable :value="certs.publicKeyPem" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Private key</h3>
|
||||
<TextareaCopyable :value="certs.privateKeyPem" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,24 +1,3 @@
|
|||
<template>
|
||||
<div>
|
||||
<n-form-item label="Your string to slugify">
|
||||
<n-input v-model:value="input" type="textarea" placeholder="Put your string here (ex: My file path)"></n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="Your slug">
|
||||
<n-input
|
||||
:value="slug"
|
||||
type="textarea"
|
||||
readonly
|
||||
placeholder="You slug will be generated here (ex: my-file-path)"
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button :disabled="slug.length === 0" @click="copy">Copy slug</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import slugify from '@sindresorhus/slugify';
|
||||
|
@ -30,4 +9,25 @@ const slug = computed(() => withDefaultOnError(() => slugify(input.value), ''));
|
|||
const { copy } = useCopy({ source: slug, text: 'Slug copied to clipboard' });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<template>
|
||||
<div>
|
||||
<n-form-item label="Your string to slugify">
|
||||
<n-input v-model:value="input" type="textarea" placeholder="Put your string here (ex: My file path)" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="Your slug">
|
||||
<n-input
|
||||
:value="slug"
|
||||
type="textarea"
|
||||
readonly
|
||||
placeholder="You slug will be generated here (ex: my-file-path)"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<div flex justify-center>
|
||||
<c-button :disabled="slug.length === 0" @click="copy">
|
||||
Copy slug
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,3 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import { type FormatFnOptions, format as formatSQL } from 'sql-formatter';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
|
||||
const inputElement = ref<HTMLElement>();
|
||||
const styleStore = useStyleStore();
|
||||
const config = reactive<Partial<FormatFnOptions>>({
|
||||
keywordCase: 'upper',
|
||||
useTabs: false,
|
||||
language: 'sql',
|
||||
indentStyle: 'standard',
|
||||
tabulateAlias: true,
|
||||
});
|
||||
|
||||
const rawSQL = ref('select field1,field2,field3 from my_table where my_condition;');
|
||||
const prettySQL = computed(() => formatSQL(rawSQL.value, config));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="flex: 0 0 100%">
|
||||
<div mx-auto style="max-width: 600px" flex gap-2 :class="{ 'flex-col': styleStore.isSmallScreen }">
|
||||
|
@ -58,30 +78,10 @@
|
|||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Prettify version of your query">
|
||||
<textarea-copyable :value="prettySQL" language="sql" :follow-height-of="inputElement" />
|
||||
<TextareaCopyable :value="prettySQL" language="sql" :follow-height-of="inputElement" />
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
import { format as formatSQL, type FormatFnOptions } from 'sql-formatter';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
|
||||
const inputElement = ref<HTMLElement>();
|
||||
const styleStore = useStyleStore();
|
||||
const config = reactive<Partial<FormatFnOptions>>({
|
||||
keywordCase: 'upper',
|
||||
useTabs: false,
|
||||
language: 'sql',
|
||||
indentStyle: 'standard',
|
||||
tabulateAlias: true,
|
||||
});
|
||||
|
||||
const rawSQL = ref('select field1,field2,field3 from my_table where my_condition;');
|
||||
const prettySQL = computed(() => formatSQL(rawSQL.value, config));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.result-card {
|
||||
position: relative;
|
||||
|
|
|
@ -1,3 +1,37 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
import { textToBase64 } from '@/utils/base64';
|
||||
|
||||
const width = ref(600);
|
||||
const height = ref(350);
|
||||
const fontSize = ref(26);
|
||||
const bgColor = ref('#cccccc');
|
||||
const fgColor = ref('#333333');
|
||||
const useExactSize = ref(true);
|
||||
const customText = ref('');
|
||||
const svgString = computed(() => {
|
||||
const w = width.value;
|
||||
const h = height.value;
|
||||
const text = customText.value.length > 0 ? customText.value : `${w}x${h}`;
|
||||
const size = useExactSize.value ? ` width="${w}" height="${h}"` : '';
|
||||
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}"${size}>
|
||||
<rect width="${w}" height="${h}" fill="${bgColor.value}"></rect>
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="monospace" font-size="${fontSize.value}px" fill="${fgColor.value}">${text}</text>
|
||||
</svg>
|
||||
`.trim();
|
||||
});
|
||||
const base64 = computed(() => `data:image/svg+xml;base64,${textToBase64(svgString.value)}`);
|
||||
|
||||
const { copy: copySVG } = useCopy({ source: svgString });
|
||||
const { copy: copyBase64 } = useCopy({ source: base64 });
|
||||
const { download } = useDownloadFileFromBase64({ source: base64 });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-form label-placement="left" label-width="100">
|
||||
|
@ -38,56 +72,28 @@
|
|||
</n-form>
|
||||
|
||||
<n-form-item label="SVG HTML element">
|
||||
<textarea-copyable :value="svgString" copy-placement="none" />
|
||||
<TextareaCopyable :value="svgString" copy-placement="none" />
|
||||
</n-form-item>
|
||||
<n-form-item label="SVG in Base64">
|
||||
<textarea-copyable :value="base64" copy-placement="none" />
|
||||
<TextareaCopyable :value="base64" copy-placement="none" />
|
||||
</n-form-item>
|
||||
|
||||
<div flex justify-center gap-3>
|
||||
<c-button @click="copySVG()">Copy svg</c-button>
|
||||
<c-button @click="copyBase64()">Copy base64</c-button>
|
||||
<c-button @click="download()">Download svg</c-button>
|
||||
<c-button @click="copySVG()">
|
||||
Copy svg
|
||||
</c-button>
|
||||
<c-button @click="copyBase64()">
|
||||
Copy base64
|
||||
</c-button>
|
||||
<c-button @click="download()">
|
||||
Download svg
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img :src="base64" alt="Image" />
|
||||
<img :src="base64" alt="Image">
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||
import { textToBase64 } from '@/utils/base64';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const width = ref(600);
|
||||
const height = ref(350);
|
||||
const fontSize = ref(26);
|
||||
const bgColor = ref('#cccccc');
|
||||
const fgColor = ref('#333333');
|
||||
const useExactSize = ref(true);
|
||||
const customText = ref('');
|
||||
const svgString = computed(() => {
|
||||
const w = width.value;
|
||||
const h = height.value;
|
||||
const text = customText.value.length > 0 ? customText.value : `${w}x${h}`;
|
||||
const size = useExactSize.value ? ` width="${w}" height="${h}"` : '';
|
||||
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}"${size}>
|
||||
<rect width="${w}" height="${h}" fill="${bgColor.value}"></rect>
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="monospace" font-size="${fontSize.value}px" fill="${fgColor.value}">${text}</text>
|
||||
</svg>
|
||||
`.trim();
|
||||
});
|
||||
const base64 = computed(() => 'data:image/svg+xml;base64,' + textToBase64(svgString.value));
|
||||
|
||||
const { copy: copySVG } = useCopy({ source: svgString });
|
||||
const { copy: copyBase64 } = useCopy({ source: base64 });
|
||||
const { download } = useDownloadFileFromBase64({ source: base64 });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-input-number {
|
||||
width: 100%;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue