Compare commits
11 Commits
renovate/a
...
v2023.11.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d5a67d96d | ||
|
|
8174db9cf3 | ||
|
|
e164afb664 | ||
|
|
d0136962b9 | ||
|
|
015c673e09 | ||
|
|
99b1eb944d | ||
|
|
cc3425dc77 | ||
|
|
681f7bf644 | ||
|
|
f5eb7a8c49 | ||
|
|
abb8335041 | ||
|
|
020e9cbe41 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
playwright-report
|
||||
coverage
|
||||
dist
|
||||
test-results
|
||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -2,6 +2,65 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## Version 2023.11.01-e164afb
|
||||
|
||||
### Features
|
||||
- **command-palette**: clear prompt on palette close (#708) (d013696)
|
||||
- **command-palette**: added about page in command palette (99b1eb9)
|
||||
- **new tool**: random MAC address generator (#657) (cc3425d)
|
||||
- **case-converter**: added mocking case (#705) (681f7bf)
|
||||
- **date-converter**: added excel date time format (#704) (f5eb7a8)
|
||||
- **i18n**: token generator (#688) (02e68d3)
|
||||
- **i18n**: home page (#687) (00562ed)
|
||||
- **i18n**: support for i18n in .ts files (#683) (ebb4ec4)
|
||||
- **i18n**: tool card (#682) (84a4a64)
|
||||
- **i18n**: about page (#680) (a2b53c2)
|
||||
- **i18n**: 404 page (#679) (35563b8)
|
||||
- **new tool**: text to ascii converter (#669) (b2ad4f7)
|
||||
- **new tool**: ULID generator (#623) (5c4d775)
|
||||
- **new tool**: add wifi qr code generator (#599) (0eedce6)
|
||||
- **new tool**: iban validation and parser (#591) (3a63837)
|
||||
- **new tool**: text diff and comparator (#588) (81bfe57)
|
||||
|
||||
### Bug fixes
|
||||
- **deps**: fix issue on slugify (#593) (#673) (720201a)
|
||||
- **deps**: update dependency monaco-editor to ^0.43.0 (#620) (e371ef7)
|
||||
- **deps**: update dependency sql-formatter to v13 (#606) (c7d4562)
|
||||
|
||||
### Refactoring
|
||||
- **ui**: better ui demo preview menu (#664) (015c673)
|
||||
- **color-converter**: improved color-converter UX (#701) (abb8335)
|
||||
- **docker**: improved docker config (#700) (020e9cb)
|
||||
- **c-table**: added description on c-table for accessibility (b408df8)
|
||||
- **ci**: reduced timeout in e2e (#666) (88b8818)
|
||||
- **ui**: new c-table ui component (#665) (ee4c853)
|
||||
- **ui**: removed n-page-header component in user-agent parser (#663) (cbf58fd)
|
||||
- **ui**: removed n-p components in about page (#662) (a757a51)
|
||||
- **ui**: switched naive tooltip components to custom ones (#661) (025f556)
|
||||
- **spelling**: minor corrections to phrasing/spelling (#596) (8a30b6b)
|
||||
- **i18n**: merge tools scoped locales with global ones (#612) (233d556)
|
||||
- **c-key-value-list**: got rid of table for layout (#611) (7ab9204)
|
||||
- **CI**: run e2e against built app and no longer vercel (#610) (18dd140)
|
||||
- **bcrypt**: fix typo (#604) (e18bae1)
|
||||
|
||||
### Chores
|
||||
- **deps**: clean unused dependencies (#709) (e164afb)
|
||||
- **deps**: update docker/setup-qemu-action action to v3 (#627) (4365226)
|
||||
- **deps**: update docker/setup-buildx-action action to v3 (#626) (57ecda1)
|
||||
- **deps**: update docker/login-action action to v3 (#625) (d8d7a3b)
|
||||
- **deps**: update docker/build-push-action action to v5 (#624) (d36b18f)
|
||||
- **deps**: update dependency node to v18.18.2 (#674) (eea9f91)
|
||||
- **deps**: update dependency node to v18.18.0 (#636) (2d2dffb)
|
||||
- **deps**: update actions/checkout action to v4 (#613) (4972159)
|
||||
- **deps**: update dependency unplugin-icons to ^0.17.0 (#609) (f035f48)
|
||||
- **deps**: update dependency @intlify/unplugin-vue-i18n to ^0.13.0 (#597) (d1dff42)
|
||||
- **deps**: update dependency @antfu/eslint-config to ^0.41.0 (#585) (a9cd91c)
|
||||
- **deps**: update dependency typescript to ~5.2.0 (#587) (f3e14fc)
|
||||
|
||||
### Doc
|
||||
- **readme**: added contributors list (#622) (557b304)
|
||||
- **hosting**: added cloudron in the other hosting solutions section (#589) (06c3547)
|
||||
|
||||
## Version 2023.08.21-6f93cba
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
# build stage
|
||||
FROM node:lts-alpine AS build-stage
|
||||
# Set environment variables for non-interactive npm installs
|
||||
ENV NPM_CONFIG_LOGLEVEL warn
|
||||
ENV CI true
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN npm install -g pnpm && pnpm i --frozen-lockfile
|
||||
COPY . .
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm i --frozen-lockfile
|
||||
RUN pnpm build
|
||||
|
||||
# production stage
|
||||
FROM nginx:stable-alpine AS production-stage
|
||||
FROM nginxinc/nginx-unprivileged:stable-alpine AS production-stage
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
|
||||
6
components.d.ts
vendored
6
components.d.ts
vendored
@@ -92,7 +92,6 @@ declare module '@vue/runtime-core' {
|
||||
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
||||
IconMdiEye: typeof import('~icons/mdi/eye')['default']
|
||||
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
|
||||
IconMdiFavoriteFilled: typeof import('~icons/mdi/favorite-filled')['default']
|
||||
IconMdiHeart: typeof import('~icons/mdi/heart')['default']
|
||||
IconMdiPause: typeof import('~icons/mdi/pause')['default']
|
||||
IconMdiPlay: typeof import('~icons/mdi/play')['default']
|
||||
@@ -116,6 +115,7 @@ declare module '@vue/runtime-core' {
|
||||
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default']
|
||||
ListConverter: typeof import('./src/tools/list-converter/list-converter.vue')['default']
|
||||
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
|
||||
MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default']
|
||||
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
|
||||
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
|
||||
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
|
||||
@@ -150,8 +150,6 @@ declare module '@vue/runtime-core' {
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NP: typeof import('naive-ui')['NP']
|
||||
NPageHeader: typeof import('naive-ui')['NPageHeader']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
@@ -159,7 +157,6 @@ declare module '@vue/runtime-core' {
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTable: typeof import('naive-ui')['NTable']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NUpload: typeof import('naive-ui')['NUpload']
|
||||
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
||||
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
|
||||
@@ -191,7 +188,6 @@ declare module '@vue/runtime-core' {
|
||||
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
|
||||
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
|
||||
UlidGenerator: typeof import('./src/tools/ulid-generator/ulid-generator.vue')['default']
|
||||
Unnamed: typeof import('./src/ui/unnamed/unnamed.vue')['default']
|
||||
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
|
||||
UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default']
|
||||
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']
|
||||
|
||||
13
package.json
13
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "it-tools",
|
||||
"version": "2023.8.21-6f93cba",
|
||||
"version": "2023.11.1-e164afb",
|
||||
"description": "Collection of handy online tools for developers, with great UX. ",
|
||||
"keywords": [
|
||||
"productivity",
|
||||
@@ -38,9 +38,9 @@
|
||||
"@it-tools/bip39": "^0.0.4",
|
||||
"@it-tools/oggen": "^1.3.0",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@tiptap/pm": "^2.1.6",
|
||||
"@tiptap/starter-kit": "^2.1.6",
|
||||
"@tiptap/vue-3": "^2.0.3",
|
||||
"@tiptap/pm": "2.1.6",
|
||||
"@tiptap/starter-kit": "2.1.6",
|
||||
"@tiptap/vue-3": "2.0.3",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"@vicons/tabler": "^0.12.0",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
@@ -68,14 +68,13 @@
|
||||
"mathjs": "^11.9.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"monaco-editor": "^0.43.0",
|
||||
"naive-ui": "^2.34.3",
|
||||
"naive-ui": "^2.35.0",
|
||||
"netmask": "^2.0.2",
|
||||
"node-forge": "^1.3.1",
|
||||
"oui": "^12.0.52",
|
||||
"pinia": "^2.0.34",
|
||||
"plausible-tracker": "^0.3.8",
|
||||
"qrcode": "^1.5.1",
|
||||
"randombytes": "^2.1.0",
|
||||
"sql-formatter": "^13.0.0",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"ulid": "^2.3.0",
|
||||
@@ -105,7 +104,6 @@
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node-forge": "^1.3.2",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/randombytes": "^2.0.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@unocss/eslint-config": "^0.55.0",
|
||||
@@ -115,7 +113,6 @@
|
||||
"@vue/runtime-dom": "^3.3.4",
|
||||
"@vue/test-utils": "^2.3.2",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"c8": "^8.0.0",
|
||||
"consola": "^3.0.2",
|
||||
"eslint": "^8.47.0",
|
||||
"hygen": "^6.2.11",
|
||||
|
||||
1793
pnpm-lock.yaml
generated
1793
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import SunIcon from '~icons/mdi/white-balance-sunny';
|
||||
import GithubIcon from '~icons/mdi/github';
|
||||
import BugIcon from '~icons/mdi/bug-outline';
|
||||
import DiceIcon from '~icons/mdi/dice-5';
|
||||
import InfoIcon from '~icons/mdi/information-outline';
|
||||
|
||||
export const useCommandPaletteStore = defineStore('command-palette', () => {
|
||||
const toolStore = useToolStore();
|
||||
@@ -61,6 +62,14 @@ export const useCommandPaletteStore = defineStore('command-palette', () => {
|
||||
keywords: ['report', 'issue', 'bug', 'problem', 'error'],
|
||||
icon: BugIcon,
|
||||
},
|
||||
{
|
||||
name: 'About',
|
||||
description: 'Learn more about IT-Tools.',
|
||||
to: '/about',
|
||||
category: 'Pages',
|
||||
keywords: ['about', 'learn', 'more', 'info', 'information'],
|
||||
icon: InfoIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const { searchResult } = useFuzzySearch({
|
||||
|
||||
@@ -37,6 +37,7 @@ function open() {
|
||||
|
||||
function close() {
|
||||
isModalOpen.value = false;
|
||||
searchPrompt.value = '';
|
||||
}
|
||||
|
||||
const selectedOptionIndex = ref(0);
|
||||
|
||||
@@ -73,6 +73,13 @@ const formats = computed(() => [
|
||||
label: 'Snakecase:',
|
||||
value: snakeCase(input.value, baseConfig),
|
||||
},
|
||||
{
|
||||
label: 'Mockingcase:',
|
||||
value: noCase(input.value, baseConfig)
|
||||
.split('')
|
||||
.map((char, index) => (index % 2 === 0 ? char.toUpperCase() : char.toLowerCase()))
|
||||
.join(''),
|
||||
},
|
||||
]);
|
||||
|
||||
const inputLabelAlignmentConfig = {
|
||||
|
||||
23
src/tools/color-converter/color-converter.e2e.spec.ts
Normal file
23
src/tools/color-converter/color-converter.e2e.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Tool - Color converter', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/color-converter');
|
||||
});
|
||||
|
||||
test('Has title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Color converter - IT Tools');
|
||||
});
|
||||
|
||||
test('Color is converted from its name to other formats', async ({ page }) => {
|
||||
await page.getByTestId('input-name').fill('olive');
|
||||
|
||||
expect(await page.getByTestId('input-name').inputValue()).toEqual('olive');
|
||||
expect(await page.getByTestId('input-hex').inputValue()).toEqual('#808000');
|
||||
expect(await page.getByTestId('input-rgb').inputValue()).toEqual('rgb(128, 128, 0)');
|
||||
expect(await page.getByTestId('input-hsl').inputValue()).toEqual('hsl(60, 100%, 25%)');
|
||||
expect(await page.getByTestId('input-hwb').inputValue()).toEqual('hwb(60 0% 50%)');
|
||||
expect(await page.getByTestId('input-cmyk').inputValue()).toEqual('device-cmyk(0% 0% 100% 50%)');
|
||||
expect(await page.getByTestId('input-lch').inputValue()).toEqual('lch(52.15% 56.81 99.57)');
|
||||
});
|
||||
});
|
||||
13
src/tools/color-converter/color-converter.models.test.ts
Normal file
13
src/tools/color-converter/color-converter.models.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { removeAlphaChannelWhenOpaque } from './color-converter.models';
|
||||
|
||||
describe('color-converter models', () => {
|
||||
describe('removeAlphaChannelWhenOpaque', () => {
|
||||
it('remove alpha channel of an hex color when it is opaque (alpha = 1)', () => {
|
||||
expect(removeAlphaChannelWhenOpaque('#000000ff')).toBe('#000000');
|
||||
expect(removeAlphaChannelWhenOpaque('#ffffffFF')).toBe('#ffffff');
|
||||
expect(removeAlphaChannelWhenOpaque('#000000FE')).toBe('#000000FE');
|
||||
expect(removeAlphaChannelWhenOpaque('#00000000')).toBe('#00000000');
|
||||
});
|
||||
});
|
||||
});
|
||||
52
src/tools/color-converter/color-converter.models.ts
Normal file
52
src/tools/color-converter/color-converter.models.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { type Colord, colord } from 'colord';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
|
||||
export { removeAlphaChannelWhenOpaque, buildColorFormat };
|
||||
|
||||
function removeAlphaChannelWhenOpaque(hexColor: string) {
|
||||
return hexColor.replace(/^(#(?:[0-9a-f]{3}){1,2})ff$/i, '$1');
|
||||
}
|
||||
|
||||
function buildColorFormat({
|
||||
label,
|
||||
parse = value => colord(value),
|
||||
format,
|
||||
placeholder,
|
||||
invalidMessage = `Invalid ${label.toLowerCase()} format.`,
|
||||
type = 'text',
|
||||
}: {
|
||||
label: string
|
||||
parse?: (value: string) => Colord
|
||||
format: (value: Colord) => string
|
||||
placeholder?: string
|
||||
invalidMessage?: string
|
||||
type?: 'text' | 'color-picker'
|
||||
}) {
|
||||
const value = ref('');
|
||||
|
||||
return {
|
||||
type,
|
||||
label,
|
||||
parse: (v: string) => withDefaultOnError(() => parse(v), undefined),
|
||||
format,
|
||||
placeholder,
|
||||
value,
|
||||
validation: useValidation({
|
||||
source: value,
|
||||
rules: [
|
||||
{
|
||||
message: invalidMessage,
|
||||
validator: v => withDefaultOnError(() => {
|
||||
if (v === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parse(v).isValid();
|
||||
}, false),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
};
|
||||
}
|
||||
@@ -1,87 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import type { Colord } from 'colord';
|
||||
import { colord, extend } from 'colord';
|
||||
|
||||
import _ from 'lodash';
|
||||
import cmykPlugin from 'colord/plugins/cmyk';
|
||||
import hwbPlugin from 'colord/plugins/hwb';
|
||||
import namesPlugin from 'colord/plugins/names';
|
||||
import lchPlugin from 'colord/plugins/lch';
|
||||
import InputCopyable from '../../components/InputCopyable.vue';
|
||||
import { buildColorFormat } from './color-converter.models';
|
||||
|
||||
extend([cmykPlugin, hwbPlugin, namesPlugin, lchPlugin]);
|
||||
|
||||
const name = ref('');
|
||||
const hex = ref('#1ea54cff');
|
||||
const rgb = ref('');
|
||||
const hsl = ref('');
|
||||
const hwb = ref('');
|
||||
const cmyk = ref('');
|
||||
const lch = ref('');
|
||||
const formats = {
|
||||
picker: buildColorFormat({
|
||||
label: 'color picker',
|
||||
format: (v: Colord) => v.toHex(),
|
||||
type: 'color-picker',
|
||||
}),
|
||||
hex: buildColorFormat({
|
||||
label: 'hex',
|
||||
format: (v: Colord) => v.toHex(),
|
||||
placeholder: 'e.g. #ff0000',
|
||||
}),
|
||||
rgb: buildColorFormat({
|
||||
label: 'rgb',
|
||||
format: (v: Colord) => v.toRgbString(),
|
||||
placeholder: 'e.g. rgb(255, 0, 0)',
|
||||
}),
|
||||
hsl: buildColorFormat({
|
||||
label: 'hsl',
|
||||
format: (v: Colord) => v.toHslString(),
|
||||
placeholder: 'e.g. hsl(0, 100%, 50%)',
|
||||
}),
|
||||
hwb: buildColorFormat({
|
||||
label: 'hwb',
|
||||
format: (v: Colord) => v.toHwbString(),
|
||||
placeholder: 'e.g. hwb(0, 0%, 0%)',
|
||||
}),
|
||||
lch: buildColorFormat({
|
||||
label: 'lch',
|
||||
format: (v: Colord) => v.toLchString(),
|
||||
placeholder: 'e.g. lch(53.24, 104.55, 40.85)',
|
||||
}),
|
||||
cmyk: buildColorFormat({
|
||||
label: 'cmyk',
|
||||
format: (v: Colord) => v.toCmykString(),
|
||||
placeholder: 'e.g. cmyk(0, 100%, 100%, 0)',
|
||||
}),
|
||||
name: buildColorFormat({
|
||||
label: 'name',
|
||||
format: (v: Colord) => v.toName({ closest: true }) ?? 'Unknown',
|
||||
placeholder: 'e.g. red',
|
||||
}),
|
||||
};
|
||||
|
||||
function onInputUpdated(value: string, omit: string) {
|
||||
try {
|
||||
const color = colord(value);
|
||||
updateColorValue(colord('#1ea54c'));
|
||||
|
||||
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();
|
||||
}
|
||||
function updateColorValue(value: Colord | undefined, omitLabel?: string) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
catch {
|
||||
//
|
||||
|
||||
if (!value.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.forEach(formats, ({ value: valueRef, format }, key) => {
|
||||
if (key !== omitLabel) {
|
||||
valueRef.value = format(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onInputUpdated(hex.value, 'hex');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card>
|
||||
<n-form label-width="100" label-placement="left">
|
||||
<n-form-item label="color picker:">
|
||||
<template v-for="({ label, parse, placeholder, validation, type }, key) in formats" :key="key">
|
||||
<input-copyable
|
||||
v-if="type === 'text'"
|
||||
v-model:value="formats[key].value.value"
|
||||
:test-id="`input-${key}`"
|
||||
:label="`${label}:`"
|
||||
label-position="left"
|
||||
label-width="100px"
|
||||
label-align="right"
|
||||
:placeholder="placeholder"
|
||||
:validation="validation"
|
||||
raw-text
|
||||
clearable
|
||||
mt-2
|
||||
@update:value="(v:string) => updateColorValue(parse(v), key)"
|
||||
/>
|
||||
|
||||
<n-form-item v-else-if="type === 'color-picker'" :label="`${label}:`" label-width="100" label-placement="left" :show-feedback="false">
|
||||
<n-color-picker
|
||||
v-model:value="hex"
|
||||
v-model:value="formats[key].value.value"
|
||||
placement="bottom-end"
|
||||
@update:value="(v: string) => onInputUpdated(v, 'hex')"
|
||||
@update:value="(v:string) => updateColorValue(parse(v), key)"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
</c-card>
|
||||
</template>
|
||||
|
||||
@@ -29,5 +29,6 @@ test.describe('Date time converter - json to yaml', () => {
|
||||
expect((await page.getByTestId('Timestamp').inputValue()).trim()).toEqual('1681333824000');
|
||||
expect((await page.getByTestId('UTC format').inputValue()).trim()).toEqual('Wed, 12 Apr 2023 21:10:24 GMT');
|
||||
expect((await page.getByTestId('Mongo ObjectID').inputValue()).trim()).toEqual('64371e400000000000000000');
|
||||
expect((await page.getByTestId('Excel date/time').inputValue()).trim()).toEqual('45028.88222222222');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import {
|
||||
dateToExcelFormat,
|
||||
excelFormatToDate,
|
||||
isExcelFormat,
|
||||
isISO8601DateTimeString,
|
||||
isISO9075DateString,
|
||||
isMongoObjectId,
|
||||
@@ -139,4 +142,39 @@ describe('date-time-converter models', () => {
|
||||
expect(isMongoObjectId('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExcelFormat', () => {
|
||||
test('an Excel format string is a floating number that can be negative', () => {
|
||||
expect(isExcelFormat('0')).toBe(true);
|
||||
expect(isExcelFormat('1')).toBe(true);
|
||||
expect(isExcelFormat('1.1')).toBe(true);
|
||||
expect(isExcelFormat('-1.1')).toBe(true);
|
||||
expect(isExcelFormat('-1')).toBe(true);
|
||||
|
||||
expect(isExcelFormat('')).toBe(false);
|
||||
expect(isExcelFormat('foo')).toBe(false);
|
||||
expect(isExcelFormat('1.1.1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dateToExcelFormat', () => {
|
||||
test('a date in Excel format is the number of days since 01/01/1900', () => {
|
||||
expect(dateToExcelFormat(new Date('2016-05-20T00:00:00.000Z'))).toBe('42510');
|
||||
expect(dateToExcelFormat(new Date('2016-05-20T12:00:00.000Z'))).toBe('42510.5');
|
||||
expect(dateToExcelFormat(new Date('2023-10-31T09:26:06.421Z'))).toBe('45230.39312987268');
|
||||
expect(dateToExcelFormat(new Date('1970-01-01T00:00:00.000Z'))).toBe('25569');
|
||||
expect(dateToExcelFormat(new Date('1800-01-01T00:00:00.000Z'))).toBe('-36522');
|
||||
});
|
||||
});
|
||||
|
||||
describe('excelFormatToDate', () => {
|
||||
test('a date in Excel format is the number of days since 01/01/1900', () => {
|
||||
expect(excelFormatToDate('0')).toEqual(new Date('1899-12-30T00:00:00.000Z'));
|
||||
expect(excelFormatToDate('1')).toEqual(new Date('1899-12-31T00:00:00.000Z'));
|
||||
expect(excelFormatToDate('2')).toEqual(new Date('1900-01-01T00:00:00.000Z'));
|
||||
expect(excelFormatToDate('4242.4242')).toEqual(new Date('1911-08-12T10:10:50.880Z'));
|
||||
expect(excelFormatToDate('42738.22626859954')).toEqual(new Date('2017-01-03T05:25:49.607Z'));
|
||||
expect(excelFormatToDate('-1000')).toEqual(new Date('1897-04-04T00:00:00.000Z'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,9 @@ export {
|
||||
isTimestamp,
|
||||
isUTCDateString,
|
||||
isMongoObjectId,
|
||||
dateToExcelFormat,
|
||||
excelFormatToDate,
|
||||
isExcelFormat,
|
||||
};
|
||||
|
||||
const ISO8601_REGEX
|
||||
@@ -21,6 +24,8 @@ const RFC3339_REGEX
|
||||
|
||||
const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/;
|
||||
|
||||
const EXCEL_FORMAT_REGEX = /^-?\d+(\.\d+)?$/;
|
||||
|
||||
function createRegexMatcher(regex: RegExp) {
|
||||
return (date?: string) => !_.isNil(date) && regex.test(date);
|
||||
}
|
||||
@@ -33,6 +38,8 @@ const isUnixTimestamp = createRegexMatcher(/^[0-9]{1,10}$/);
|
||||
const isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/);
|
||||
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
|
||||
|
||||
const isExcelFormat = createRegexMatcher(EXCEL_FORMAT_REGEX);
|
||||
|
||||
function isUTCDateString(date?: string) {
|
||||
if (_.isNil(date)) {
|
||||
return false;
|
||||
@@ -45,3 +52,11 @@ function isUTCDateString(date?: string) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function dateToExcelFormat(date: Date) {
|
||||
return String(((date.getTime()) / (1000 * 60 * 60 * 24)) + 25569);
|
||||
}
|
||||
|
||||
function excelFormatToDate(excelFormat: string | number) {
|
||||
return new Date((Number(excelFormat) - 25569) * 86400 * 1000);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
} from 'date-fns';
|
||||
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
|
||||
import {
|
||||
dateToExcelFormat,
|
||||
excelFormatToDate,
|
||||
isExcelFormat,
|
||||
isISO8601DateTimeString,
|
||||
isISO9075DateString,
|
||||
isMongoObjectId,
|
||||
@@ -85,6 +88,12 @@ const formats: DateFormat[] = [
|
||||
toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000),
|
||||
formatMatcher: date => isMongoObjectId(date),
|
||||
},
|
||||
{
|
||||
name: 'Excel date/time',
|
||||
fromDate: date => dateToExcelFormat(date),
|
||||
toDate: excelFormatToDate,
|
||||
formatMatcher: isExcelFormat,
|
||||
},
|
||||
];
|
||||
|
||||
const formatIndex = ref(6);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { tool as base64FileConverter } from './base64-file-converter';
|
||||
import { tool as base64StringConverter } from './base64-string-converter';
|
||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
||||
import { tool as macAddressGenerator } from './mac-address-generator';
|
||||
import { tool as textToBinary } from './text-to-binary';
|
||||
import { tool as ulidGenerator } from './ulid-generator';
|
||||
import { tool as ibanValidatorAndParser } from './iban-validator-and-parser';
|
||||
@@ -140,7 +141,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||
},
|
||||
{
|
||||
name: 'Network',
|
||||
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, ipv6UlaGenerator],
|
||||
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, macAddressGenerator, ipv6UlaGenerator],
|
||||
},
|
||||
{
|
||||
name: 'Math',
|
||||
|
||||
12
src/tools/mac-address-generator/index.ts
Normal file
12
src/tools/mac-address-generator/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Devices } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'MAC address generator',
|
||||
path: '/mac-address-generator',
|
||||
description: 'Enter the quantity and prefix. MAC addresses will be generated in your chosen case (uppercase or lowercase)',
|
||||
keywords: ['mac', 'address', 'generator', 'random', 'prefix'],
|
||||
component: () => import('./mac-address-generator.vue'),
|
||||
icon: Devices,
|
||||
createdAt: new Date('2023-11-31'),
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Tool - MAC address generator', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/mac-address-generator');
|
||||
});
|
||||
|
||||
test('Has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('MAC address generator - IT Tools');
|
||||
});
|
||||
});
|
||||
103
src/tools/mac-address-generator/mac-address-generator.vue
Normal file
103
src/tools/mac-address-generator/mac-address-generator.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { generateRandomMacAddress } from './mac-adress-generator.models';
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
import { useCopy } from '@/composable/copy';
|
||||
import { usePartialMacAddressValidation } from '@/utils/macAddress';
|
||||
|
||||
const amount = useStorage('mac-address-generator-amount', 1);
|
||||
const macAddressPrefix = useStorage('mac-address-generator-prefix', '64:16:7F');
|
||||
|
||||
const prefixValidation = usePartialMacAddressValidation(macAddressPrefix);
|
||||
|
||||
const casesTransformers = [
|
||||
{ label: 'Uppercase', value: (value: string) => value.toUpperCase() },
|
||||
{ label: 'Lowercase', value: (value: string) => value.toLowerCase() },
|
||||
];
|
||||
const caseTransformer = ref(casesTransformers[0].value);
|
||||
|
||||
const separators = [
|
||||
{
|
||||
label: ':',
|
||||
value: ':',
|
||||
},
|
||||
{
|
||||
label: '-',
|
||||
value: '-',
|
||||
},
|
||||
{
|
||||
label: '.',
|
||||
value: '.',
|
||||
},
|
||||
{
|
||||
label: 'None',
|
||||
value: '',
|
||||
},
|
||||
];
|
||||
const separator = useStorage('mac-address-generator-separator', separators[0].value);
|
||||
|
||||
const [macAddresses, refreshMacAddresses] = computedRefreshable(() => {
|
||||
if (!prefixValidation.isValid) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const ids = _.times(amount.value, () => caseTransformer.value(generateRandomMacAddress({
|
||||
prefix: macAddressPrefix.value,
|
||||
separator: separator.value,
|
||||
})));
|
||||
return ids.join('\n');
|
||||
});
|
||||
|
||||
const { copy } = useCopy({ source: macAddresses, text: 'MAC addresses copied to the clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex flex-col justify-center gap-2>
|
||||
<div flex items-center>
|
||||
<label w-150px pr-12px text-right> Quantity:</label>
|
||||
<n-input-number v-model:value="amount" min="1" max="100" flex-1 />
|
||||
</div>
|
||||
|
||||
<c-input-text
|
||||
v-model:value="macAddressPrefix"
|
||||
label="MAC address prefix:"
|
||||
placeholder="Set a prefix, e.g. 64:16:7F"
|
||||
clearable
|
||||
label-position="left"
|
||||
spellcheck="false"
|
||||
:validation="prefixValidation"
|
||||
raw-text
|
||||
label-width="150px"
|
||||
label-align="right"
|
||||
/>
|
||||
|
||||
<c-buttons-select
|
||||
v-model:value="caseTransformer"
|
||||
:options="casesTransformers"
|
||||
label="Case:"
|
||||
label-width="150px"
|
||||
label-align="right"
|
||||
/>
|
||||
|
||||
<c-buttons-select
|
||||
v-model:value="separator"
|
||||
:options="separators"
|
||||
label="Separator:"
|
||||
label-width="150px"
|
||||
label-align="right"
|
||||
/>
|
||||
|
||||
<c-card mt-5 flex data-test-id="ulids">
|
||||
<pre m-0 m-x-auto>{{ macAddresses }}</pre>
|
||||
</c-card>
|
||||
|
||||
<div flex justify-center gap-2>
|
||||
<c-button data-test-id="refresh" @click="refreshMacAddresses()">
|
||||
Refresh
|
||||
</c-button>
|
||||
<c-button @click="copy()">
|
||||
Copy
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { generateRandomMacAddress, splitPrefix } from './mac-adress-generator.models';
|
||||
|
||||
describe('mac-adress-generator models', () => {
|
||||
describe('splitPrefix', () => {
|
||||
it('a mac address prefix is splitted around non hex characters', () => {
|
||||
expect(splitPrefix('')).toEqual([]);
|
||||
expect(splitPrefix('01')).toEqual(['01']);
|
||||
expect(splitPrefix('01:')).toEqual(['01']);
|
||||
expect(splitPrefix('01:23')).toEqual(['01', '23']);
|
||||
expect(splitPrefix('01-23')).toEqual(['01', '23']);
|
||||
});
|
||||
|
||||
it('when a prefix contains only hex characters, they are grouped by 2', () => {
|
||||
expect(splitPrefix('0123')).toEqual(['01', '23']);
|
||||
expect(splitPrefix('012345')).toEqual(['01', '23', '45']);
|
||||
expect(splitPrefix('0123456')).toEqual(['01', '23', '45', '06']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateRandomMacAddress', () => {
|
||||
const createRandomByteGenerator = () => {
|
||||
let i = 0;
|
||||
return () => (i++).toString(16).padStart(2, '0');
|
||||
};
|
||||
|
||||
it('generates a random mac address', () => {
|
||||
expect(generateRandomMacAddress({ getRandomByte: createRandomByteGenerator() })).toBe('00:01:02:03:04:05');
|
||||
});
|
||||
|
||||
it('generates a random mac address with a prefix', () => {
|
||||
expect(generateRandomMacAddress({ prefix: 'ff:ee:aa', getRandomByte: createRandomByteGenerator() })).toBe('ff:ee:aa:00:01:02');
|
||||
expect(generateRandomMacAddress({ prefix: 'ff:ee:a', getRandomByte: createRandomByteGenerator() })).toBe('ff:ee:0a:00:01:02');
|
||||
});
|
||||
|
||||
it('generates a random mac address with a prefix and a different separator', () => {
|
||||
expect(generateRandomMacAddress({ prefix: 'ff-ee-aa', separator: '-', getRandomByte: createRandomByteGenerator() })).toBe('ff-ee-aa-00-01-02');
|
||||
expect(generateRandomMacAddress({ prefix: 'ff:ee:aa', separator: '-', getRandomByte: createRandomByteGenerator() })).toBe('ff-ee-aa-00-01-02');
|
||||
expect(generateRandomMacAddress({ prefix: 'ff-ee:aa', separator: '-', getRandomByte: createRandomByteGenerator() })).toBe('ff-ee-aa-00-01-02');
|
||||
expect(generateRandomMacAddress({ prefix: 'ff ee:aa', separator: '-', getRandomByte: createRandomByteGenerator() })).toBe('ff-ee-aa-00-01-02');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export { splitPrefix, generateRandomMacAddress };
|
||||
|
||||
function splitPrefix(prefix: string): string[] {
|
||||
const base = prefix.match(/[^0-9a-f]/i) === null ? prefix.match(/.{1,2}/g) ?? [] : prefix.split(/[^0-9a-f]/i);
|
||||
|
||||
return base.filter(Boolean).map(byte => byte.padStart(2, '0'));
|
||||
}
|
||||
|
||||
function generateRandomMacAddress({ prefix: rawPrefix = '', separator = ':', getRandomByte = () => _.random(0, 255).toString(16).padStart(2, '0') }: { prefix?: string; separator?: string; getRandomByte?: () => string } = {}) {
|
||||
const prefix = splitPrefix(rawPrefix);
|
||||
|
||||
const randomBytes = _.times(6 - prefix.length, getRandomByte);
|
||||
const bytes = [...prefix, ...randomBytes];
|
||||
|
||||
return bytes.join(separator);
|
||||
}
|
||||
@@ -4,10 +4,8 @@ import { demoRoutes } from './demo.routes';
|
||||
|
||||
<template>
|
||||
<div grid grid-cols-5 gap-2>
|
||||
<c-card v-for="{ name } of demoRoutes" :key="name" :title="String(name)">
|
||||
<c-button :to="{ name }">
|
||||
{{ name }}
|
||||
</c-button>
|
||||
</c-card>
|
||||
<c-button v-for="{ name } of demoRoutes" :key="name" :to="{ name }">
|
||||
{{ name }}
|
||||
</c-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,7 @@ const componentName = computed(() => _.startCase(String(route.name).replace(/^c-
|
||||
<h1>c-lib components</h1>
|
||||
|
||||
<div flex>
|
||||
<div w-30 b-r b-gray b-op-10 b-r-solid pr-4>
|
||||
<div w-200px b-r b-gray b-op-10 b-r-solid pr-4>
|
||||
<c-button
|
||||
v-for="{ name } of demoRoutes"
|
||||
:key="name"
|
||||
@@ -20,6 +20,7 @@ const componentName = computed(() => _.startCase(String(route.name).replace(/^c-
|
||||
:to="{ name }"
|
||||
w-full
|
||||
important:justify-start
|
||||
important:text-left
|
||||
:type="route.name === name ? 'primary' : 'default'"
|
||||
>
|
||||
{{ name }}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import DemoHome from './demo-home.page.vue';
|
||||
|
||||
const demoPages = import.meta.glob('../*/*.demo.vue');
|
||||
const demoPages = import.meta.glob('../*/*.demo.vue', { eager: true });
|
||||
|
||||
export const demoRoutes = Object.keys(demoPages).map((path) => {
|
||||
const [, , fileName] = path.split('/');
|
||||
const name = fileName.split('.').shift();
|
||||
export const demoRoutes = Object.keys(demoPages).map((demoComponentPath) => {
|
||||
const [, , fileName] = demoComponentPath.split('/');
|
||||
const demoComponentName = fileName.split('.').shift();
|
||||
|
||||
return {
|
||||
path: name,
|
||||
name,
|
||||
component: () => import(/* @vite-ignore */ path),
|
||||
path: demoComponentName,
|
||||
name: demoComponentName,
|
||||
component: () => import(/* @vite-ignore */ demoComponentPath),
|
||||
} as RouteRecordRaw;
|
||||
});
|
||||
|
||||
|
||||
@@ -15,4 +15,18 @@ function macAddressValidation(value: Ref) {
|
||||
});
|
||||
}
|
||||
|
||||
export { macAddressValidation, macAddressValidationRules };
|
||||
const partialMacAddressValidationRules = [
|
||||
{
|
||||
message: 'Invalid partial MAC address',
|
||||
validator: (value: string) => value.trim().match(/^([0-9a-f]{2}[:\-. ]){0,5}([0-9a-f]{0,2})$/i),
|
||||
},
|
||||
];
|
||||
|
||||
function usePartialMacAddressValidation(value: Ref) {
|
||||
return useValidation({
|
||||
source: value,
|
||||
rules: partialMacAddressValidationRules,
|
||||
});
|
||||
}
|
||||
|
||||
export { macAddressValidation, macAddressValidationRules, usePartialMacAddressValidation, partialMacAddressValidationRules };
|
||||
|
||||
Reference in New Issue
Block a user