diff --git a/components.d.ts b/components.d.ts index 076fc061..b88ae981 100644 --- a/components.d.ts +++ b/components.d.ts @@ -32,6 +32,7 @@ declare module '@vue/runtime-core' { Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default'] 'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] + CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default'] CLabel: typeof import('./src/ui/c-label/c-label.vue')['default'] CLink: typeof import('./src/ui/c-link/c-link.vue')['default'] 'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] @@ -45,6 +46,10 @@ declare module '@vue/runtime-core' { CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] CSelect: typeof import('./src/ui/c-select/c-select.vue')['default'] 'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default'] + CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default'] + 'CTextCopyable.demo': typeof import('./src/ui/c-text-copyable/c-text-copyable.demo.vue')['default'] + CTooltip: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default'] + 'CTooltip.demo': typeof import('./src/ui/c-tooltip/c-tooltip.demo.vue')['default'] DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] 'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default'] DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] @@ -68,6 +73,7 @@ declare module '@vue/runtime-core' { HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default'] HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default'] HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] + IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default'] 'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] 'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default'] 'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] diff --git a/package.json b/package.json index b68e04f7..1cad277b 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "fuse.js": "^6.6.2", "highlight.js": "^11.7.0", "iarna-toml-esm": "^3.0.5", + "ibantools": "^4.3.3", "json5": "^2.2.3", "jwt-decode": "^3.1.2", "libphonenumber-js": "^1.10.28", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ceb775e..0c2b5ca8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ dependencies: iarna-toml-esm: specifier: ^3.0.5 version: 3.0.5 + ibantools: + specifier: ^4.3.3 + version: 4.3.3 json5: specifier: ^2.2.3 version: 2.2.3 @@ -5845,6 +5848,10 @@ packages: stream: 0.0.2 dev: false + /ibantools@4.3.3: + resolution: {integrity: sha512-RUTlGuFj3cU/Qfu5YIrsIZjW34/VDgKOz5fDr64Mc4NWP9b2i48vQ39r5xCl1yyFQeyEG/lASstIQHAUX18rRA==} + dev: false + /iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} 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..3501543f --- /dev/null +++ b/src/tools/iban-validator-and-parser/iban-validator-and-parser.e2e.spec.ts @@ -0,0 +1,51 @@ +import { type Page, expect, test } from '@playwright/test'; +import _ from 'lodash'; + +async function extractIbanInfo({ page }: { page: Page }) { + const tdHandles = await page.locator('table tr td').elementHandles(); + const tdTextContents = await Promise.all(tdHandles.map(el => el.textContent())); + + return _.chain(tdTextContents) + .map(tdTextContent => tdTextContent?.trim().replace(' Copy to clipboard', '')) + .chunk(2) + .value(); +} + +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 checksumWrong IBAN checksum Copy to clipboard'], + ['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..d5cdc022 --- /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 39b34ad6..15770b56 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -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 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'; @@ -151,7 +152,7 @@ export const toolsByCategory: ToolCategory[] = [ }, { name: 'Data', - components: [phoneParserAndFormatter], + components: [phoneParserAndFormatter, ibanValidatorAndParser], }, ]; 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..e3b19afd --- /dev/null +++ b/src/ui/c-key-value-list/c-key-value-list.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/ui/c-text-copyable/c-text-copyable.demo.vue b/src/ui/c-text-copyable/c-text-copyable.demo.vue new file mode 100644 index 00000000..e2aeb1da --- /dev/null +++ b/src/ui/c-text-copyable/c-text-copyable.demo.vue @@ -0,0 +1,3 @@ + diff --git a/src/ui/c-text-copyable/c-text-copyable.vue b/src/ui/c-text-copyable/c-text-copyable.vue new file mode 100644 index 00000000..b78e4cda --- /dev/null +++ b/src/ui/c-text-copyable/c-text-copyable.vue @@ -0,0 +1,17 @@ + + + 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 @@ + diff --git a/src/ui/c-tooltip/c-tooltip.vue b/src/ui/c-tooltip/c-tooltip.vue new file mode 100644 index 00000000..cc48fe1c --- /dev/null +++ b/src/ui/c-tooltip/c-tooltip.vue @@ -0,0 +1,27 @@ + + +