diff --git a/src/tools/benchmark-builder/benchmark-builder.models.ts b/src/tools/benchmark-builder/benchmark-builder.models.ts
index 10ae8a78..0bf3456b 100644
--- a/src/tools/benchmark-builder/benchmark-builder.models.ts
+++ b/src/tools/benchmark-builder/benchmark-builder.models.ts
@@ -18,7 +18,7 @@ function computeVariance({ data }: { data: number[] }) {
return computeAverage({ data: squaredDiffs });
}
-function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; headerMap?: Record
}) {
+function arrayToMarkdownTable({ data, headerMap = {} }: { data: Record[]; headerMap?: Record }) {
if (!Array.isArray(data) || data.length === 0) {
return '';
}
diff --git a/src/tools/benchmark-builder/benchmark-builder.vue b/src/tools/benchmark-builder/benchmark-builder.vue
index 91f3155d..7922791c 100644
--- a/src/tools/benchmark-builder/benchmark-builder.vue
+++ b/src/tools/benchmark-builder/benchmark-builder.vue
@@ -1,10 +1,11 @@
-
+
diff --git a/src/tools/iban-validator-and-parser/iban-validator-and-parser.e2e.spec.ts b/src/tools/iban-validator-and-parser/iban-validator-and-parser.e2e.spec.ts
new file mode 100644
index 00000000..c4a99860
--- /dev/null
+++ b/src/tools/iban-validator-and-parser/iban-validator-and-parser.e2e.spec.ts
@@ -0,0 +1,52 @@
+import { type Page, expect, test } from '@playwright/test';
+
+async function extractIbanInfo({ page }: { page: Page }) {
+ const itemsLines = await page
+ .locator('.c-key-value-list__item').all();
+
+ return await Promise.all(
+ itemsLines.map(async item => [
+ (await item.locator('.c-key-value-list__key').textContent() ?? '').trim(),
+ (await item.locator('.c-key-value-list__value').textContent() ?? '').trim(),
+ ]),
+ );
+}
+
+test.describe('Tool - Iban validator and parser', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/iban-validator-and-parser');
+ });
+
+ test('Has correct title', async ({ page }) => {
+ await expect(page).toHaveTitle('IBAN validator and parser - IT Tools');
+ });
+
+ test('iban info are extracted from a valid iban', async ({ page }) => {
+ await page.getByTestId('iban-input').fill('DE89370400440532013000');
+
+ const ibanInfo = await extractIbanInfo({ page });
+
+ expect(ibanInfo).toEqual([
+ ['Is IBAN valid ?', 'Yes'],
+ ['Is IBAN a QR-IBAN ?', 'No'],
+ ['Country code', 'DE'],
+ ['BBAN', '370400440532013000'],
+ ['IBAN friendly format', 'DE89 3704 0044 0532 0130 00'],
+ ]);
+ });
+
+ test('invalid iban errors are displayed', async ({ page }) => {
+ await page.getByTestId('iban-input').fill('FR7630006060011234567890189');
+
+ const ibanInfo = await extractIbanInfo({ page });
+
+ expect(ibanInfo).toEqual([
+ ['Is IBAN valid ?', 'No'],
+ ['IBAN errors', 'Wrong account bank branch checksum Wrong IBAN checksum'],
+ ['Is IBAN a QR-IBAN ?', 'No'],
+ ['Country code', 'N/A'],
+ ['BBAN', 'N/A'],
+ ['IBAN friendly format', 'FR76 3000 6060 0112 3456 7890 189'],
+ ]);
+ });
+});
diff --git a/src/tools/iban-validator-and-parser/iban-validator-and-parser.service.ts b/src/tools/iban-validator-and-parser/iban-validator-and-parser.service.ts
new file mode 100644
index 00000000..bde71dba
--- /dev/null
+++ b/src/tools/iban-validator-and-parser/iban-validator-and-parser.service.ts
@@ -0,0 +1,18 @@
+import { ValidationErrorsIBAN } from 'ibantools';
+
+export { getFriendlyErrors };
+
+const ibanErrorToMessage = {
+ [ValidationErrorsIBAN.NoIBANProvided]: 'No IBAN provided',
+ [ValidationErrorsIBAN.NoIBANCountry]: 'No IBAN country',
+ [ValidationErrorsIBAN.WrongBBANLength]: 'Wrong BBAN length',
+ [ValidationErrorsIBAN.WrongBBANFormat]: 'Wrong BBAN format',
+ [ValidationErrorsIBAN.ChecksumNotNumber]: 'Checksum is not a number',
+ [ValidationErrorsIBAN.WrongIBANChecksum]: 'Wrong IBAN checksum',
+ [ValidationErrorsIBAN.WrongAccountBankBranchChecksum]: 'Wrong account bank branch checksum',
+ [ValidationErrorsIBAN.QRIBANNotAllowed]: 'QR-IBAN not allowed',
+};
+
+function getFriendlyErrors(errorCodes: ValidationErrorsIBAN[]) {
+ return errorCodes.map(errorCode => ibanErrorToMessage[errorCode]).filter(Boolean);
+}
diff --git a/src/tools/iban-validator-and-parser/iban-validator-and-parser.vue b/src/tools/iban-validator-and-parser/iban-validator-and-parser.vue
new file mode 100644
index 00000000..647be983
--- /dev/null
+++ b/src/tools/iban-validator-and-parser/iban-validator-and-parser.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
diff --git a/src/tools/iban-validator-and-parser/index.ts b/src/tools/iban-validator-and-parser/index.ts
new file mode 100644
index 00000000..b0cae50d
--- /dev/null
+++ b/src/tools/iban-validator-and-parser/index.ts
@@ -0,0 +1,12 @@
+import { defineTool } from '../tool';
+import Bank from '~icons/mdi/bank';
+
+export const tool = defineTool({
+ name: 'IBAN validator and parser',
+ path: '/iban-validator-and-parser',
+ description: 'Validate and parse IBAN numbers. Check if IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format.',
+ keywords: ['iban', 'validator', 'and', 'parser', 'bic', 'bank'],
+ component: () => import('./iban-validator-and-parser.vue'),
+ icon: Bank,
+ createdAt: new Date('2023-08-26'),
+});
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 0cfc1739..cc5f42ee 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -1,6 +1,10 @@
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 ulidGenerator } from './ulid-generator';
+import { tool as ibanValidatorAndParser } from './iban-validator-and-parser';
+import { tool as stringObfuscator } from './string-obfuscator';
+import { tool as textDiff } from './text-diff';
import { tool as emojiPicker } from './emoji-picker';
import { tool as passwordStrengthAnalyser } from './password-strength-analyser';
import { tool as yamlToToml } from './yaml-to-toml';
@@ -53,6 +57,7 @@ import { tool as metaTagGenerator } from './meta-tag-generator';
import { tool as mimeTypes } from './mime-types';
import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
import { tool as qrCodeGenerator } from './qr-code-generator';
+import { tool as wifiQrCodeGenerator } from './wifi-qr-code-generator';
import { tool as randomPortGenerator } from './random-port-generator';
import { tool as romanNumeralConverter } from './roman-numeral-converter';
import { tool as sqlPrettify } from './sql-prettify';
@@ -70,7 +75,7 @@ import { tool as xmlFormatter } from './xml-formatter';
export const toolsByCategory: ToolCategory[] = [
{
name: 'Crypto',
- components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser],
+ components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser],
},
{
name: 'Converter',
@@ -114,7 +119,7 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Images and videos',
- components: [qrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
+ components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
},
{
name: 'Development',
@@ -145,11 +150,11 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Text',
- components: [loremIpsumGenerator, textStatistics, emojiPicker],
+ components: [loremIpsumGenerator, textStatistics, emojiPicker, stringObfuscator, textDiff],
},
{
name: 'Data',
- components: [phoneParserAndFormatter],
+ components: [phoneParserAndFormatter, ibanValidatorAndParser],
},
];
diff --git a/src/tools/ipv4-address-converter/ipv4-address-converter.service.ts b/src/tools/ipv4-address-converter/ipv4-address-converter.service.ts
index 1ef487eb..903ff5bb 100644
--- a/src/tools/ipv4-address-converter/ipv4-address-converter.service.ts
+++ b/src/tools/ipv4-address-converter/ipv4-address-converter.service.ts
@@ -23,7 +23,7 @@ function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: str
+ _.chain(ip)
.trim()
.split('.')
- .map(part => parseInt(part).toString(16).padStart(2, '0'))
+ .map(part => Number.parseInt(part).toString(16).padStart(2, '0'))
.chunk(2)
.map(blocks => blocks.join(''))
.join(':')
diff --git a/src/tools/ipv4-range-expander/ipv4-range-expander.service.ts b/src/tools/ipv4-range-expander/ipv4-range-expander.service.ts
index 78fbde5b..14761f59 100644
--- a/src/tools/ipv4-range-expander/ipv4-range-expander.service.ts
+++ b/src/tools/ipv4-range-expander/ipv4-range-expander.service.ts
@@ -13,7 +13,7 @@ function getRangesize(start: string, end: string) {
return -1;
}
- return 1 + parseInt(end, 2) - parseInt(start, 2);
+ return 1 + Number.parseInt(end, 2) - Number.parseInt(start, 2);
}
function getCidr(start: string, end: string) {
@@ -55,8 +55,8 @@ function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) {
const cidr = getCidr(start, end);
if (cidr != null) {
const result: Ipv4RangeExpanderResult = {};
- result.newEnd = bits2ip(parseInt(cidr.end, 2));
- result.newStart = bits2ip(parseInt(cidr.start, 2));
+ result.newEnd = bits2ip(Number.parseInt(cidr.end, 2));
+ result.newStart = bits2ip(Number.parseInt(cidr.start, 2));
result.newCidr = `${result.newStart}/${cidr.mask}`;
result.newSize = getRangesize(cidr.start, cidr.end);
diff --git a/src/tools/jwt-parser/jwt-parser.service.ts b/src/tools/jwt-parser/jwt-parser.service.ts
index 19edc5f2..cc39145a 100644
--- a/src/tools/jwt-parser/jwt-parser.service.ts
+++ b/src/tools/jwt-parser/jwt-parser.service.ts
@@ -1,6 +1,5 @@
import jwtDecode, { type JwtHeader, type JwtPayload } from 'jwt-decode';
import _ from 'lodash';
-import { match } from 'ts-pattern';
import { ALGORITHM_DESCRIPTIONS, CLAIM_DESCRIPTIONS } from './jwt-parser.constants';
export { decodeJwt };
@@ -32,10 +31,15 @@ function parseClaims({ claim, value }: { claim: string; value: unknown }) {
}
function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) {
- return match(claim)
- .with('exp', 'nbf', 'iat', () => dateFormatter(value))
- .with('alg', () => (_.isString(value) ? ALGORITHM_DESCRIPTIONS[value] : undefined))
- .otherwise(() => undefined);
+ if (['exp', 'nbf', 'iat'].includes(claim)) {
+ return dateFormatter(value);
+ }
+
+ if (claim === 'alg' && _.isString(value)) {
+ return ALGORITHM_DESCRIPTIONS[value];
+ }
+
+ return undefined;
}
function dateFormatter(value: unknown) {
diff --git a/src/tools/mac-address-lookup/mac-address-lookup.vue b/src/tools/mac-address-lookup/mac-address-lookup.vue
index ef0927d5..82628805 100644
--- a/src/tools/mac-address-lookup/mac-address-lookup.vue
+++ b/src/tools/mac-address-lookup/mac-address-lookup.vue
@@ -8,7 +8,7 @@ const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '')
const macAddress = ref('20:37:06:12:34:56');
const details = computed(() => db[getVendorValue(macAddress.value)]);
-const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' });
+const { copy } = useCopy({ source: () => details.value ?? '', text: 'Vendor info copied to the clipboard' });
diff --git a/src/tools/otp-code-generator-and-validator/otp.service.ts b/src/tools/otp-code-generator-and-validator/otp.service.ts
index fae3d37c..fb4c33c9 100644
--- a/src/tools/otp-code-generator-and-validator/otp.service.ts
+++ b/src/tools/otp-code-generator-and-validator/otp.service.ts
@@ -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 => Number.parseInt(char, 16));
}
function computeHMACSha1(message: string, key: string) {
@@ -32,7 +32,7 @@ function base32toHex(base32: string) {
.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 => Number.parseInt(chunk, 2).toString(16).padStart(2, '0')).join('');
return hex;
}
diff --git a/src/tools/otp-code-generator-and-validator/token-display.vue b/src/tools/otp-code-generator-and-validator/token-display.vue
index a40c9af9..5313b0be 100644
--- a/src/tools/otp-code-generator-and-validator/token-display.vue
+++ b/src/tools/otp-code-generator-and-validator/token-display.vue
@@ -1,10 +1,10 @@
diff --git a/src/tools/password-strength-analyser/password-strength-analyser.e2e.spec.ts b/src/tools/password-strength-analyser/password-strength-analyser.e2e.spec.ts
index a694c547..9a3c9319 100644
--- a/src/tools/password-strength-analyser/password-strength-analyser.e2e.spec.ts
+++ b/src/tools/password-strength-analyser/password-strength-analyser.e2e.spec.ts
@@ -14,6 +14,6 @@ test.describe('Tool - Password strength analyser', () => {
const crackDuration = await page.getByTestId('crack-duration').textContent();
- expect(crackDuration).toEqual('15,091 milleniums, 3 centurys');
+ expect(crackDuration).toEqual('15,091 millennia, 3 centuries');
});
});
diff --git a/src/tools/password-strength-analyser/password-strength-analyser.service.ts b/src/tools/password-strength-analyser/password-strength-analyser.service.ts
index 7197b001..aa281848 100644
--- a/src/tools/password-strength-analyser/password-strength-analyser.service.ts
+++ b/src/tools/password-strength-analyser/password-strength-analyser.service.ts
@@ -4,7 +4,7 @@ export { getPasswordCrackTimeEstimation, getCharsetLength };
function prettifyExponentialNotation(exponentialNotation: number) {
const [base, exponent] = exponentialNotation.toString().split('e');
- const baseAsNumber = parseFloat(base);
+ const baseAsNumber = Number.parseFloat(base);
const prettyBase = baseAsNumber % 1 === 0 ? baseAsNumber.toLocaleString() : baseAsNumber.toFixed(2);
return exponent ? `${prettyBase}e${exponent}` : prettyBase;
}
@@ -19,20 +19,20 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) {
}
const timeUnits = [
- { unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation },
- { unit: 'century', secondsInUnit: 3153600000 },
- { unit: 'decade', secondsInUnit: 315360000 },
- { unit: 'year', secondsInUnit: 31536000 },
- { unit: 'month', secondsInUnit: 2592000 },
- { unit: 'week', secondsInUnit: 604800 },
- { unit: 'day', secondsInUnit: 86400 },
- { unit: 'hour', secondsInUnit: 3600 },
- { unit: 'minute', secondsInUnit: 60 },
- { unit: 'second', secondsInUnit: 1 },
+ { unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation, plural: 'millennia' },
+ { unit: 'century', secondsInUnit: 3153600000, plural: 'centuries' },
+ { unit: 'decade', secondsInUnit: 315360000, plural: 'decades' },
+ { unit: 'year', secondsInUnit: 31536000, plural: 'years' },
+ { unit: 'month', secondsInUnit: 2592000, plural: 'months' },
+ { unit: 'week', secondsInUnit: 604800, plural: 'weeks' },
+ { unit: 'day', secondsInUnit: 86400, plural: 'days' },
+ { unit: 'hour', secondsInUnit: 3600, plural: 'hours' },
+ { unit: 'minute', secondsInUnit: 60, plural: 'minutes' },
+ { unit: 'second', secondsInUnit: 1, plural: 'seconds' },
];
return _.chain(timeUnits)
- .map(({ unit, secondsInUnit, format = _.identity }) => {
+ .map(({ unit, secondsInUnit, plural, format = _.identity }) => {
const quantity = Math.floor(seconds / secondsInUnit);
seconds %= secondsInUnit;
@@ -41,7 +41,7 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) {
}
const formattedQuantity = format(quantity);
- return `${formattedQuantity} ${unit}${quantity > 1 ? 's' : ''}`;
+ return `${formattedQuantity} ${quantity > 1 ? plural : unit}`;
})
.compact()
.take(2)
diff --git a/src/tools/roman-numeral-converter/roman-numeral-converter.vue b/src/tools/roman-numeral-converter/roman-numeral-converter.vue
index d365cc5a..498c340c 100644
--- a/src/tools/roman-numeral-converter/roman-numeral-converter.vue
+++ b/src/tools/roman-numeral-converter/roman-numeral-converter.vue
@@ -36,7 +36,7 @@ const validationRoman = useValidation({
});
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' });
+const { copy: copyArabic } = useCopy({ source: () => String(outputNumeral), text: 'Arabic number copied to the clipboard' });
diff --git a/src/tools/sql-prettify/sql-prettify.vue b/src/tools/sql-prettify/sql-prettify.vue
index 049d6c89..503cd15f 100644
--- a/src/tools/sql-prettify/sql-prettify.vue
+++ b/src/tools/sql-prettify/sql-prettify.vue
@@ -1,11 +1,11 @@
+
+
+
+
+
+
+
+
+
+ {{ obfuscatedString }}
+
+
+
+
+
+
+
+
diff --git a/src/tools/text-diff/index.ts b/src/tools/text-diff/index.ts
new file mode 100644
index 00000000..992acbae
--- /dev/null
+++ b/src/tools/text-diff/index.ts
@@ -0,0 +1,12 @@
+import { FileDiff } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+ name: 'Text diff',
+ path: '/text-diff',
+ description: 'Compare two texts and see the differences between them.',
+ keywords: ['text', 'diff', 'compare', 'string', 'text diff', 'code'],
+ component: () => import('./text-diff.vue'),
+ icon: FileDiff,
+ createdAt: new Date('2023-08-16'),
+});
diff --git a/src/tools/text-diff/text-diff.vue b/src/tools/text-diff/text-diff.vue
new file mode 100644
index 00000000..990f05b1
--- /dev/null
+++ b/src/tools/text-diff/text-diff.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/tools/token-generator/token-generator.service.ts b/src/tools/token-generator/token-generator.service.ts
index 1885d249..3733a884 100644
--- a/src/tools/token-generator/token-generator.service.ts
+++ b/src/tools/token-generator/token-generator.service.ts
@@ -15,14 +15,12 @@ export function createToken({
length?: number
alphabet?: string
}) {
- const allAlphabet
- = alphabet
- ?? [
- ...(withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : ''),
- ...(withLowercase ? 'abcdefghijklmopqrstuvwxyz' : ''),
- ...(withNumbers ? '0123456789' : ''),
- ...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : ''),
- ].join('');
+ const allAlphabet = alphabet ?? [
+ withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : '',
+ withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '',
+ withNumbers ? '0123456789' : '',
+ withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '',
+ ].join(''); ;
return shuffleString(allAlphabet.repeat(length)).substring(0, length);
}
diff --git a/src/tools/ulid-generator/index.ts b/src/tools/ulid-generator/index.ts
new file mode 100644
index 00000000..6a5408dd
--- /dev/null
+++ b/src/tools/ulid-generator/index.ts
@@ -0,0 +1,12 @@
+import { SortDescendingNumbers } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+ name: 'ULID generator',
+ path: '/ulid-generator',
+ description: 'Generate random Universally Unique Lexicographically Sortable Identifier (ULID).',
+ keywords: ['ulid', 'generator', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'],
+ component: () => import('./ulid-generator.vue'),
+ icon: SortDescendingNumbers,
+ createdAt: new Date('2023-09-11'),
+});
diff --git a/src/tools/ulid-generator/ulid-generator.e2e.spec.ts b/src/tools/ulid-generator/ulid-generator.e2e.spec.ts
new file mode 100644
index 00000000..34473376
--- /dev/null
+++ b/src/tools/ulid-generator/ulid-generator.e2e.spec.ts
@@ -0,0 +1,23 @@
+import { expect, test } from '@playwright/test';
+
+const ULID_REGEX = /[0-9A-Z]{26}/;
+
+test.describe('Tool - ULID generator', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/ulid-generator');
+ });
+
+ test('Has correct title', async ({ page }) => {
+ await expect(page).toHaveTitle('ULID generator - IT Tools');
+ });
+
+ test('the refresh button generates a new ulid', async ({ page }) => {
+ const ulid = await page.getByTestId('ulids').textContent();
+ expect(ulid?.trim()).toMatch(ULID_REGEX);
+
+ await page.getByTestId('refresh').click();
+ const newUlid = await page.getByTestId('ulids').textContent();
+ expect(ulid?.trim()).not.toBe(newUlid?.trim());
+ expect(newUlid?.trim()).toMatch(ULID_REGEX);
+ });
+});
diff --git a/src/tools/ulid-generator/ulid-generator.vue b/src/tools/ulid-generator/ulid-generator.vue
new file mode 100644
index 00000000..06e695ef
--- /dev/null
+++ b/src/tools/ulid-generator/ulid-generator.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+ Quantity:
+
+
+
+
+
+
+ {{ ulids }}
+
+
+
+
+ Refresh
+
+
+ Copy
+
+
+
+
diff --git a/src/tools/uuid-generator/index.ts b/src/tools/uuid-generator/index.ts
index 2b4b3d34..ae5ae0da 100644
--- a/src/tools/uuid-generator/index.ts
+++ b/src/tools/uuid-generator/index.ts
@@ -5,7 +5,7 @@ export const tool = defineTool({
name: 'UUIDs v4 generator',
path: '/uuid-generator',
description:
- 'A universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot !).',
+ 'A Universally Unique Identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot!).',
keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'],
component: () => import('./uuid-generator.vue'),
icon: Fingerprint,
diff --git a/src/tools/uuid-generator/uuid-generator.vue b/src/tools/uuid-generator/uuid-generator.vue
index a393781b..e9f55e6b 100644
--- a/src/tools/uuid-generator/uuid-generator.vue
+++ b/src/tools/uuid-generator/uuid-generator.vue
@@ -34,7 +34,7 @@ const { copy } = useCopy({ source: uuids, text: 'UUIDs copied to the clipboard'
/>
-
+
Copy
diff --git a/src/tools/wifi-qr-code-generator/index.ts b/src/tools/wifi-qr-code-generator/index.ts
new file mode 100644
index 00000000..ad0135c3
--- /dev/null
+++ b/src/tools/wifi-qr-code-generator/index.ts
@@ -0,0 +1,13 @@
+import { Qrcode } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+ name: 'WiFi QR Code generator',
+ path: '/wifi-qrcode-generator',
+ description:
+ 'Generate and download QR-codes for quick connections to WiFi networks.',
+ keywords: ['qr', 'code', 'generator', 'square', 'color', 'link', 'low', 'medium', 'quartile', 'high', 'transparent', 'wifi'],
+ component: () => import('./wifi-qr-code-generator.vue'),
+ icon: Qrcode,
+ createdAt: new Date('2023-09-06'),
+});
diff --git a/src/tools/wifi-qr-code-generator/useQRCode.ts b/src/tools/wifi-qr-code-generator/useQRCode.ts
new file mode 100644
index 00000000..c8a7215c
--- /dev/null
+++ b/src/tools/wifi-qr-code-generator/useQRCode.ts
@@ -0,0 +1,146 @@
+import { type MaybeRef, get } from '@vueuse/core';
+import QRCode, { type QRCodeToDataURLOptions } from 'qrcode';
+import { isRef, ref, watch } from 'vue';
+
+export const wifiEncryptions = ['WEP', 'WPA', 'nopass', 'WPA2-EAP'] as const;
+export type WifiEncryption = typeof wifiEncryptions[number];
+
+// @see https://en.wikipedia.org/wiki/Extensible_Authentication_Protocol
+// for a list of available EAP methods. There are a lot (40!) of them.
+export const EAPMethods = [
+ 'MD5',
+ 'POTP',
+ 'GTC',
+ 'TLS',
+ 'IKEv2',
+ 'SIM',
+ 'AKA',
+ 'AKA\'',
+ 'TTLS',
+ 'PWD',
+ 'LEAP',
+ 'PSK',
+ 'FAST',
+ 'TEAP',
+ 'EKE',
+ 'NOOB',
+ 'PEAP',
+] as const;
+export type EAPMethod = typeof EAPMethods[number];
+
+export const EAPPhase2Methods = [
+ 'None',
+ 'MSCHAPV2',
+] as const;
+export type EAPPhase2Method = typeof EAPPhase2Methods[number];
+
+interface IWifiQRCodeOptions {
+ ssid: MaybeRef
+ password: MaybeRef
+ eapMethod: MaybeRef
+ isHiddenSSID: MaybeRef
+ eapAnonymous: MaybeRef
+ eapIdentity: MaybeRef
+ eapPhase2Method: MaybeRef
+ color: { foreground: MaybeRef; background: MaybeRef }
+ options?: QRCodeToDataURLOptions
+}
+
+interface GetQrCodeTextOptions {
+ ssid: string
+ password: string
+ encryption: WifiEncryption
+ eapMethod: EAPMethod
+ isHiddenSSID: boolean
+ eapAnonymous: boolean
+ eapIdentity: string
+ eapPhase2Method: EAPPhase2Method
+}
+
+function escapeString(str: string) {
+ // replaces \, ;, ,, " and : with the same character preceded by a backslash
+ return str.replace(/([\\;,:"])/g, '\\$1');
+}
+
+function getQrCodeText(options: GetQrCodeTextOptions): string | null {
+ const { ssid, password, encryption, eapMethod, isHiddenSSID, eapAnonymous, eapIdentity, eapPhase2Method } = options;
+ if (!ssid) {
+ return null;
+ }
+ if (encryption === 'nopass') {
+ return `WIFI:S:${escapeString(ssid)};;`; // type can be omitted in that case, and password is not needed, makes the QR Code smaller
+ }
+ if (encryption !== 'WPA2-EAP' && password) {
+ // EAP has a lot of options, so we'll handle it separately
+ // WPA and WEP are pretty simple though.
+ return `WIFI:S:${escapeString(ssid)};T:${encryption};P:${escapeString(password)};${isHiddenSSID ? 'H:true' : ''};`;
+ }
+ if (encryption === 'WPA2-EAP' && password && eapMethod) {
+ // WPA2-EAP string is a lot more complex, first off, we drop the text if there is no identity, and it's not anonymous.
+ if (!eapIdentity && !eapAnonymous) {
+ return null;
+ }
+ // From reading, I could only find that a phase 2 is required for the PEAP method, I may be wrong though, I didn't read the whole spec.
+ if (eapMethod === 'PEAP' && !eapPhase2Method) {
+ return null;
+ }
+ // The string is built in the following order:
+ // 1. SSID
+ // 2. Authentication type
+ // 3. Password
+ // 4. EAP method
+ // 5. EAP phase 2 method
+ // 6. Identity or anonymous if checked
+ // 7. Hidden SSID if checked
+ const identity = eapAnonymous ? 'A:anon' : `I:${escapeString(eapIdentity)}`;
+ const phase2 = eapPhase2Method !== 'None' ? `PH2:${eapPhase2Method};` : '';
+ return `WIFI:S:${escapeString(ssid)};T:WPA2-EAP;P:${escapeString(password)};E:${eapMethod};${phase2}${identity};${isHiddenSSID ? 'H:true' : ''};`;
+ }
+ return null;
+}
+
+export function useWifiQRCode({
+ ssid,
+ password,
+ eapMethod,
+ isHiddenSSID,
+ eapAnonymous,
+ eapIdentity,
+ eapPhase2Method,
+ color: { background, foreground },
+ options,
+}: IWifiQRCodeOptions) {
+ const qrcode = ref('');
+ const encryption = ref('WPA');
+
+ watch(
+ [ssid, password, encryption, eapMethod, isHiddenSSID, eapAnonymous, eapIdentity, eapPhase2Method, background, foreground].filter(isRef),
+ async () => {
+ // @see https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
+ // This is the full spec, there's quite a bit of logic to generate the string embeddedin the QR code.
+ const text = getQrCodeText({
+ ssid: get(ssid),
+ password: get(password),
+ encryption: get(encryption),
+ eapMethod: get(eapMethod),
+ isHiddenSSID: get(isHiddenSSID),
+ eapAnonymous: get(eapAnonymous),
+ eapIdentity: get(eapIdentity),
+ eapPhase2Method: get(eapPhase2Method),
+ });
+ if (text) {
+ qrcode.value = await QRCode.toDataURL(get(text).trim(), {
+ color: {
+ dark: get(foreground),
+ light: get(background),
+ ...options?.color,
+ },
+ errorCorrectionLevel: 'M',
+ ...options,
+ });
+ }
+ },
+ { immediate: true },
+ );
+ return { qrcode, encryption };
+}
diff --git a/src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue b/src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue
new file mode 100644
index 00000000..e6320d3e
--- /dev/null
+++ b/src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+ Hidden SSID
+
+
+
+
+
+
+
+ Anonymous?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Download qr-code
+
+
+
+
+
+
diff --git a/src/ui/c-buttons-select/c-buttons-select.demo.vue b/src/ui/c-buttons-select/c-buttons-select.demo.vue
new file mode 100644
index 00000000..dea15289
--- /dev/null
+++ b/src/ui/c-buttons-select/c-buttons-select.demo.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/src/ui/c-buttons-select/c-buttons-select.types.ts b/src/ui/c-buttons-select/c-buttons-select.types.ts
new file mode 100644
index 00000000..ccb110d4
--- /dev/null
+++ b/src/ui/c-buttons-select/c-buttons-select.types.ts
@@ -0,0 +1,5 @@
+import type { CSelectOption } from '../c-select/c-select.types';
+
+export type CButtonSelectOption = CSelectOption & {
+ tooltip?: string
+};
diff --git a/src/ui/c-buttons-select/c-buttons-select.vue b/src/ui/c-buttons-select/c-buttons-select.vue
new file mode 100644
index 00000000..38fff66f
--- /dev/null
+++ b/src/ui/c-buttons-select/c-buttons-select.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
diff --git a/src/ui/c-diff-editor/c-diff-editor.vue b/src/ui/c-diff-editor/c-diff-editor.vue
new file mode 100644
index 00000000..2aa29475
--- /dev/null
+++ b/src/ui/c-diff-editor/c-diff-editor.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
diff --git a/src/ui/c-key-value-list/c-key-value-list-item.vue b/src/ui/c-key-value-list/c-key-value-list-item.vue
new file mode 100644
index 00000000..d21ef5d1
--- /dev/null
+++ b/src/ui/c-key-value-list/c-key-value-list-item.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.placeholder ?? 'N/A' }}
+
+
+
+
+
diff --git a/src/ui/c-key-value-list/c-key-value-list.types.ts b/src/ui/c-key-value-list/c-key-value-list.types.ts
new file mode 100644
index 00000000..40cc4ba4
--- /dev/null
+++ b/src/ui/c-key-value-list/c-key-value-list.types.ts
@@ -0,0 +1,9 @@
+export interface CKeyValueListItem {
+ label: string
+ value: string | string[] | number | boolean | undefined | null
+ hideOnNil?: boolean
+ placeholder?: string
+ showCopyButton?: boolean
+}
+
+export type CKeyValueListItems = CKeyValueListItem[];
diff --git a/src/ui/c-key-value-list/c-key-value-list.vue b/src/ui/c-key-value-list/c-key-value-list.vue
new file mode 100644
index 00000000..d8a2b001
--- /dev/null
+++ b/src/ui/c-key-value-list/c-key-value-list.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
diff --git a/src/ui/c-modal/c-modal.vue b/src/ui/c-modal/c-modal.vue
index af92f01a..4d032bbf 100644
--- a/src/ui/c-modal/c-modal.vue
+++ b/src/ui/c-modal/c-modal.vue
@@ -1,11 +1,17 @@
+
+
+
+
+ {{ displayedValue ?? value }}
+
+
+
+
diff --git a/src/ui/c-tooltip/c-tooltip.demo.vue b/src/ui/c-tooltip/c-tooltip.demo.vue
new file mode 100644
index 00000000..d3852573
--- /dev/null
+++ b/src/ui/c-tooltip/c-tooltip.demo.vue
@@ -0,0 +1,17 @@
+
+
+
+ Hover me
+
+
+ Tooltip content
+
+
+
+
+
+
+ Hover me
+
+
+
diff --git a/src/ui/c-tooltip/c-tooltip.vue b/src/ui/c-tooltip/c-tooltip.vue
new file mode 100644
index 00000000..095315fb
--- /dev/null
+++ b/src/ui/c-tooltip/c-tooltip.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+ {{ tooltip }}
+
+
+
+
diff --git a/src/ui/color/color.models.ts b/src/ui/color/color.models.ts
index 5b4c79b5..0ae38a48 100644
--- a/src/ui/color/color.models.ts
+++ b/src/ui/color/color.models.ts
@@ -4,7 +4,7 @@ const clampHex = (value: number) => Math.max(0, Math.min(255, Math.round(value))
function lighten(color: string, amount: number): string {
const alpha = color.length === 9 ? color.slice(7) : '';
- const num = parseInt(color.slice(1, 7), 16);
+ const num = Number.parseInt(color.slice(1, 7), 16);
const r = clampHex(((num >> 16) & 255) + amount);
const g = clampHex(((num >> 8) & 255) + amount);
diff --git a/src/utils/convert.ts b/src/utils/convert.ts
index c897543c..9eac1921 100644
--- a/src/utils/convert.ts
+++ b/src/utils/convert.ts
@@ -7,5 +7,5 @@ export function formatBytes(bytes: number, decimals = 2) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
- return `${parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
+ return `${Number.parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
}
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 8f5064d0..181ee9ae 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -1,14 +1,20 @@
{
- "extends": "@vue/tsconfig/tsconfig.web.json",
+ "extends": "@vue/tsconfig/tsconfig.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "**/*.d.ts", "node_modules/vite-plugin-pwa/client.d.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
- "lib": ["ES2021"],
+ "lib": ["ES2022"],
+ "target": "es2022",
+ "module": "es2022",
+ "moduleResolution": "Node",
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
- "types": ["naive-ui/volar", "unplugin-icons/types/vue", "@intlify/unplugin-vue-i18n/messages"]
+ "types": ["naive-ui/volar", "@intlify/unplugin-vue-i18n/messages", "unplugin-icons/types/vue"],
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "skipLibCheck": true
}
}
diff --git a/tsconfig.vite-config.json b/tsconfig.vite-config.json
index d20d8726..b941809e 100644
--- a/tsconfig.vite-config.json
+++ b/tsconfig.vite-config.json
@@ -1,5 +1,5 @@
{
- "extends": "@vue/tsconfig/tsconfig.node.json",
+ "extends": "@tsconfig/node18/tsconfig.json",
"include": ["vite.config.*"],
"compilerOptions": {
"composite": true,
diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json
index d080d611..0d7369ad 100644
--- a/tsconfig.vitest.json
+++ b/tsconfig.vitest.json
@@ -4,6 +4,6 @@
"compilerOptions": {
"composite": true,
"lib": [],
- "types": ["node", "jsdom"]
+ "types": ["node", "jsdom", "unplugin-icons/types/vue"]
}
}
diff --git a/vite.config.ts b/vite.config.ts
index 8e2e0836..00f90c33 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -25,7 +25,7 @@ export default defineConfig({
runtimeOnly: true,
compositionOnly: true,
fullInstall: true,
- include: [resolve(__dirname, 'locales/**'), resolve(__dirname, 'src/tools/*/locales/**')],
+ include: [resolve(__dirname, 'locales/**')],
}),
AutoImport({
imports: [
@@ -106,4 +106,7 @@ export default defineConfig({
test: {
exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'],
},
+ build: {
+ target: 'esnext',
+ },
});