mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-20 14:56:17 -04:00
feat(new tool): iban validation and parser (#591)
This commit is contained in:
parent
81bfe57cb8
commit
3a63837d3d
14 changed files with 278 additions and 1 deletions
6
components.d.ts
vendored
6
components.d.ts
vendored
|
@ -32,6 +32,7 @@ declare module '@vue/runtime-core' {
|
||||||
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
|
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
|
||||||
CInputText: typeof import('./src/ui/c-input-text/c-input-text.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']
|
'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']
|
CLabel: typeof import('./src/ui/c-label/c-label.vue')['default']
|
||||||
CLink: typeof import('./src/ui/c-link/c-link.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']
|
'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']
|
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
|
||||||
CSelect: typeof import('./src/ui/c-select/c-select.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']
|
'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']
|
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']
|
'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default']
|
||||||
DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.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']
|
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
|
||||||
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.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']
|
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:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
|
||||||
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
|
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
|
||||||
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
"iarna-toml-esm": "^3.0.5",
|
"iarna-toml-esm": "^3.0.5",
|
||||||
|
"ibantools": "^4.3.3",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"libphonenumber-js": "^1.10.28",
|
"libphonenumber-js": "^1.10.28",
|
||||||
|
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
|
@ -80,6 +80,9 @@ dependencies:
|
||||||
iarna-toml-esm:
|
iarna-toml-esm:
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
|
ibantools:
|
||||||
|
specifier: ^4.3.3
|
||||||
|
version: 4.3.3
|
||||||
json5:
|
json5:
|
||||||
specifier: ^2.2.3
|
specifier: ^2.2.3
|
||||||
version: 2.2.3
|
version: 2.2.3
|
||||||
|
@ -5845,6 +5848,10 @@ packages:
|
||||||
stream: 0.0.2
|
stream: 0.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/ibantools@4.3.3:
|
||||||
|
resolution: {integrity: sha512-RUTlGuFj3cU/Qfu5YIrsIZjW34/VDgKOz5fDr64Mc4NWP9b2i48vQ39r5xCl1yyFQeyEG/lASstIQHAUX18rRA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/iconv-lite@0.6.3:
|
/iconv-lite@0.6.3:
|
||||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
|
@ -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'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { extractIBAN, friendlyFormatIBAN, isQRIBAN, validateIBAN } from 'ibantools';
|
||||||
|
import { getFriendlyErrors } from './iban-validator-and-parser.service';
|
||||||
|
import type { CKeyValueListItems } from '@/ui/c-key-value-list/c-key-value-list.types';
|
||||||
|
|
||||||
|
const rawIban = ref('');
|
||||||
|
|
||||||
|
const ibanInfo = computed<CKeyValueListItems>(() => {
|
||||||
|
const iban = rawIban.value.toUpperCase().replace(/\s/g, '').replace(/-/g, '');
|
||||||
|
|
||||||
|
if (iban === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { valid: isIbanValid, errorCodes } = validateIBAN(iban);
|
||||||
|
const { countryCode, bban } = extractIBAN(iban);
|
||||||
|
const errors = getFriendlyErrors(errorCodes);
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
{
|
||||||
|
label: 'Is IBAN valid ?',
|
||||||
|
value: isIbanValid,
|
||||||
|
showCopyButton: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'IBAN errors',
|
||||||
|
value: errors.length === 0 ? undefined : errors,
|
||||||
|
hideOnNil: true,
|
||||||
|
showCopyButton: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Is IBAN a QR-IBAN ?',
|
||||||
|
value: isQRIBAN(iban),
|
||||||
|
showCopyButton: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Country code',
|
||||||
|
value: countryCode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'BBAN',
|
||||||
|
value: bban,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'IBAN friendly format',
|
||||||
|
value: friendlyFormatIBAN(iban),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const ibanExamples = [
|
||||||
|
'FR7630006000011234567890189',
|
||||||
|
'DE89370400440532013000',
|
||||||
|
'GB29NWBK60161331926819',
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<c-input-text v-model:value="rawIban" placeholder="Enter an IBAN to check for validity..." test-id="iban-input" />
|
||||||
|
|
||||||
|
<c-key-value-list :items="ibanInfo" my-5 />
|
||||||
|
|
||||||
|
<c-card title="Valid IBAN examples">
|
||||||
|
<div v-for="iban in ibanExamples" :key="iban">
|
||||||
|
<c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" />
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
12
src/tools/iban-validator-and-parser/index.ts
Normal file
12
src/tools/iban-validator-and-parser/index.ts
Normal file
|
@ -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'),
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
import { tool as base64FileConverter } from './base64-file-converter';
|
import { tool as base64FileConverter } from './base64-file-converter';
|
||||||
import { tool as base64StringConverter } from './base64-string-converter';
|
import { tool as base64StringConverter } from './base64-string-converter';
|
||||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
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 stringObfuscator } from './string-obfuscator';
|
||||||
import { tool as textDiff } from './text-diff';
|
import { tool as textDiff } from './text-diff';
|
||||||
import { tool as emojiPicker } from './emoji-picker';
|
import { tool as emojiPicker } from './emoji-picker';
|
||||||
|
@ -151,7 +152,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Data',
|
name: 'Data',
|
||||||
components: [phoneParserAndFormatter],
|
components: [phoneParserAndFormatter, ibanValidatorAndParser],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
9
src/ui/c-key-value-list/c-key-value-list.types.ts
Normal file
9
src/ui/c-key-value-list/c-key-value-list.types.ts
Normal file
|
@ -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[];
|
37
src/ui/c-key-value-list/c-key-value-list.vue
Normal file
37
src/ui/c-key-value-list/c-key-value-list.vue
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import _ from 'lodash';
|
||||||
|
import type { CKeyValueListItems } from './c-key-value-list.types';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ items?: CKeyValueListItems }>(), { items: () => [] });
|
||||||
|
const { items } = toRefs(props);
|
||||||
|
|
||||||
|
const formattedItems = computed(() => items.value.filter(item => !_.isNil(item.value) || !item.hideOnNil));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table border-collapse table-fixed>
|
||||||
|
<tr v-for="item in formattedItems" :key="item.label">
|
||||||
|
<td py-1 pr-2 text-right font-bold>
|
||||||
|
{{ item.label }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td v-if="_.isArray(item.value)">
|
||||||
|
<div v-for="value in item.value" :key="value">
|
||||||
|
<c-text-copyable :value="value" :show-icon="item.showCopyButton ?? true" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td v-else-if="_.isBoolean(item.value)">
|
||||||
|
<c-text-copyable :value="item.value ? 'true' : 'false'" :displayed-value="item.value ? 'Yes' : 'No'" :show-icon="item.showCopyButton ?? true" />
|
||||||
|
</td>
|
||||||
|
<td v-else-if="_.isNumber(item.value)" font-mono>
|
||||||
|
<c-text-copyable :value="String(item.value)" :show-icon="item.showCopyButton ?? true" />
|
||||||
|
</td>
|
||||||
|
<td v-else-if="_.isNil(item.value) || item.value === ''" op-70>
|
||||||
|
{{ item.placeholder ?? 'N/A' }}
|
||||||
|
</td>
|
||||||
|
<td v-else>
|
||||||
|
<c-text-copyable :value="item.value" :show-icon="item.showCopyButton ?? true" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</template>
|
3
src/ui/c-text-copyable/c-text-copyable.demo.vue
Normal file
3
src/ui/c-text-copyable/c-text-copyable.demo.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<c-text-copyable value="value" displayed-value="displayedValue" />
|
||||||
|
</template>
|
17
src/ui/c-text-copyable/c-text-copyable.vue
Normal file
17
src/ui/c-text-copyable/c-text-copyable.vue
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useCopy } from '@/composable/copy';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ value?: string; displayedValue?: string; showIcon?: boolean }>(), { value: '', displayedValue: undefined, showIcon: true });
|
||||||
|
const { value, displayedValue, showIcon } = toRefs(props);
|
||||||
|
|
||||||
|
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-tooltip :tooltip="isJustCopied ? 'Copied!' : 'Copy to clipboard'" cursor-pointer @click="copy">
|
||||||
|
<span flex items-center gap-2>
|
||||||
|
{{ displayedValue ?? value }}
|
||||||
|
<icon-mdi-content-copy v-if="showIcon" op-40 />
|
||||||
|
</span>
|
||||||
|
</c-tooltip>
|
||||||
|
</template>
|
17
src/ui/c-tooltip/c-tooltip.demo.vue
Normal file
17
src/ui/c-tooltip/c-tooltip.demo.vue
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<c-tooltip>
|
||||||
|
Hover me
|
||||||
|
|
||||||
|
<template #tooltip>
|
||||||
|
Tooltip content
|
||||||
|
</template>
|
||||||
|
</c-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div mt-5>
|
||||||
|
<c-tooltip tooltip="Tooltip content">
|
||||||
|
Hover me
|
||||||
|
</c-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
27
src/ui/c-tooltip/c-tooltip.vue
Normal file
27
src/ui/c-tooltip/c-tooltip.vue
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = withDefaults(defineProps<{ tooltip?: string }>(), { tooltip: '' });
|
||||||
|
const { tooltip } = toRefs(props);
|
||||||
|
|
||||||
|
const targetRef = ref();
|
||||||
|
const isTargetHovered = useElementHover(targetRef);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative" inline-block>
|
||||||
|
<div ref="targetRef">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute bottom-100% left-50% z-10 mb-5px whitespace-nowrap rounded bg-black px-12px py-6px text-sm text-white shadow-lg transition transition transition-duration-0.2s -translate-x-1/2"
|
||||||
|
:class="{
|
||||||
|
'op-0 scale-0': isTargetHovered === false,
|
||||||
|
'op-100 scale-100': isTargetHovered,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot name="tooltip">
|
||||||
|
{{ tooltip }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
Loading…
Add table
Add a link
Reference in a new issue