chore(lint): switched to a better lint config

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,85 @@
<script setup lang="ts">
import {
chineseSimplifiedWordList,
chineseTraditionalWordList,
czechWordList,
englishWordList,
entropyToMnemonic,
frenchWordList,
generateEntropy,
italianWordList,
japaneseWordList,
koreanWordList,
mnemonicToEntropy,
portugueseWordList,
spanishWordList,
} from '@it-tools/bip39';
import { Copy, Refresh } from '@vicons/tabler';
import { computed, ref } from 'vue';
import { useCopy } from '@/composable/copy';
import { useValidation } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean';
import { withDefaultOnError } from '@/utils/defaults';
const languages = {
'English': englishWordList,
'Chinese simplified': chineseSimplifiedWordList,
'Chinese traditional': chineseTraditionalWordList,
'Czech': czechWordList,
'French': frenchWordList,
'Italian': italianWordList,
'Japanese': japaneseWordList,
'Korean': koreanWordList,
'Portuguese': portugueseWordList,
'Spanish': spanishWordList,
};
const entropy = ref(generateEntropy());
const passphraseInput = ref('');
const language = ref<keyof typeof languages>('English');
const passphrase = computed({
get() {
return withDefaultOnError(() => entropyToMnemonic(entropy.value, languages[language.value]), passphraseInput.value);
},
set(value: string) {
passphraseInput.value = value;
entropy.value = withDefaultOnError(() => mnemonicToEntropy(value, languages[language.value]), '');
},
});
const entropyValidation = useValidation({
source: entropy,
rules: [
{
validator: value => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4',
},
{
validator: value => /^[a-fA-F0-9]*$/.test(value),
message: 'Entropy should be an hexadecimal string',
},
],
});
const mnemonicValidation = useValidation({
source: passphrase,
rules: [
{
validator: value => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
message: 'Invalid mnemonic',
},
],
});
function refreshEntropy() {
entropy.value = generateEntropy();
}
const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' });
const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' });
</script>
<template>
<div>
<n-grid cols="3" x-gap="12">
@ -47,85 +129,3 @@
</n-form-item>
</div>
</template>
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import { useValidation } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean';
import { withDefaultOnError } from '@/utils/defaults';
import {
chineseSimplifiedWordList,
chineseTraditionalWordList,
czechWordList,
englishWordList,
entropyToMnemonic,
frenchWordList,
generateEntropy,
italianWordList,
japaneseWordList,
koreanWordList,
mnemonicToEntropy,
portugueseWordList,
spanishWordList,
} from '@it-tools/bip39';
import { Copy, Refresh } from '@vicons/tabler';
import { computed, ref } from 'vue';
const languages = {
English: englishWordList,
'Chinese simplified': chineseSimplifiedWordList,
'Chinese traditional': chineseTraditionalWordList,
Czech: czechWordList,
French: frenchWordList,
Italian: italianWordList,
Japanese: japaneseWordList,
Korean: koreanWordList,
Portuguese: portugueseWordList,
Spanish: spanishWordList,
};
const entropy = ref(generateEntropy());
const passphraseInput = ref('');
const language = ref<keyof typeof languages>('English');
const passphrase = computed({
get() {
return withDefaultOnError(() => entropyToMnemonic(entropy.value, languages[language.value]), passphraseInput.value);
},
set(value: string) {
passphraseInput.value = value;
entropy.value = withDefaultOnError(() => mnemonicToEntropy(value, languages[language.value]), '');
},
});
const entropyValidation = useValidation({
source: entropy,
rules: [
{
validator: (value) => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4',
},
{
validator: (value) => /^[a-fA-F0-9]*$/.test(value),
message: 'Entropy should be an hexadecimal string',
},
],
});
const mnemonicValidation = useValidation({
source: passphrase,
rules: [
{
validator: (value) => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
message: 'Invalid mnemonic',
},
],
});
function refreshEntropy() {
entropy.value = generateEntropy();
}
const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' });
const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' });
</script>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from '@playwright/test';
test.describe('Date time converter - json to yaml', () => {
test.beforeEach(async ({ page }) => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,17 @@
<script setup lang="ts">
import { escape, unescape } from 'lodash';
import { computed, ref } from 'vue';
import { useCopy } from '@/composable/copy';
const escapeInput = ref('<title>IT Tool</title>');
const escapeOutput = computed(() => escape(escapeInput.value));
const { copy: copyEscaped } = useCopy({ source: escapeOutput });
const unescapeInput = ref('&lt;title&gt;IT Tool&lt;/title');
const unescapeOutput = computed(() => unescape(unescapeInput.value));
const { copy: copyUnescaped } = useCopy({ source: unescapeOutput });
</script>
<template>
<c-card title="Escape html entities">
<n-form-item label="Your string :">
@ -20,7 +34,9 @@
</n-form-item>
<div flex justify-center>
<c-button @click="copyEscaped"> Copy </c-button>
<c-button @click="copyEscaped">
Copy
</c-button>
</div>
</c-card>
<c-card title="Unescape html entities">
@ -44,21 +60,9 @@
</n-form-item>
<div flex justify-center>
<c-button @click="copyUnescaped"> Copy </c-button>
<c-button @click="copyUnescaped">
Copy
</c-button>
</div>
</c-card>
</template>
<script setup lang="ts">
import { escape, unescape } from 'lodash';
import { computed, ref } from 'vue';
import { useCopy } from '@/composable/copy';
const escapeInput = ref('<title>IT Tool</title>');
const escapeOutput = computed(() => escape(escapeInput.value));
const { copy: copyEscaped } = useCopy({ source: escapeOutput });
const unescapeInput = ref('&lt;title&gt;IT Tool&lt;/title');
const unescapeOutput = computed(() => unescape(unescapeInput.value));
const { copy: copyUnescaped } = useCopy({ source: unescapeOutput });
</script>

View file

@ -1,14 +1,3 @@
<template>
<c-card v-if="editor" important:p0>
<menu-bar class="editor-header" :editor="editor" />
<n-divider style="margin-top: 0" />
<div px8 pb6>
<editor-content class="editor-content" :editor="editor" />
</div>
</c-card>
</template>
<script setup lang="ts">
import { tryOnBeforeUnmount, useVModel } from '@vueuse/core';
import { Editor, EditorContent } from '@tiptap/vue-3';
@ -16,9 +5,9 @@ import StarterKit from '@tiptap/starter-kit';
import { useThemeVars } from 'naive-ui';
import MenuBar from './menu-bar.vue';
const themeVars = useThemeVars();
const props = defineProps<{ html: string }>();
const emit = defineEmits(['update:html']);
const themeVars = useThemeVars();
const html = useVModel(props, 'html', emit);
const editor = new Editor({
@ -33,6 +22,17 @@ tryOnBeforeUnmount(() => {
});
</script>
<template>
<c-card v-if="editor" important:p0>
<MenuBar class="editor-header" :editor="editor" />
<n-divider style="margin-top: 0" />
<div px8 pb6>
<EditorContent class="editor-content" :editor="editor" />
</div>
</c-card>
</template>
<style scoped lang="less">
::v-deep(.ProseMirror-focused) {
outline: none;

View file

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

View file

@ -1,12 +1,3 @@
<template>
<div flex items-center>
<template v-for="(item, index) in items">
<n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical />
<menu-bar-item v-else-if="item.type === 'button'" :key="index" v-bind="item" />
</template>
</div>
</template>
<script setup lang="ts">
import type { Editor } from '@tiptap/vue-3';
import {
@ -27,7 +18,7 @@ import {
Strikethrough,
TextWrap,
} from '@vicons/tabler';
import { toRefs, type Component } from 'vue';
import { type Component, toRefs } from 'vue';
import MenuBarItem from './menu-bar-item.vue';
const props = defineProps<{ editor: Editor }>();
@ -35,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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from '@playwright/test';
test.describe('Tool - json to yaml', () => {
test.beforeEach(async ({ page }) => {
@ -14,6 +14,6 @@ test.describe('Tool - json to yaml', () => {
const generatedJson = await page.getByTestId('area-content').innerText();
expect(generatedJson.trim()).toEqual(`foo: bar\nlist:\n - item\n - key: value`.trim());
expect(generatedJson.trim()).toEqual('foo: bar\nlist:\n - item\n - key: value'.trim());
});
});

View file

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

View file

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

View file

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

View file

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

View file

@ -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;

View file

@ -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>

View file

@ -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\'');
});
});

View file

@ -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';

View file

@ -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();
}

View file

@ -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
}

View file

@ -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>

View file

@ -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');
}

View file

@ -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>

View file

@ -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>

View file

@ -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',

View file

@ -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>

View file

@ -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)[]
}

View file

@ -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;

View file

@ -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',

View file

@ -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>

View file

@ -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;

View file

@ -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');

View file

@ -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', () => {

View file

@ -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,

View file

@ -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;

View file

@ -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 }) => {

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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 },
);

View file

@ -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;

View file

@ -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', () => {

View file

@ -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);

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -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