aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components.d.ts6
-rw-r--r--package.json1
-rw-r--r--pnpm-lock.yaml7
-rw-r--r--src/tools/iban-validator-and-parser/iban-validator-and-parser.e2e.spec.ts51
-rw-r--r--src/tools/iban-validator-and-parser/iban-validator-and-parser.service.ts18
-rw-r--r--src/tools/iban-validator-and-parser/iban-validator-and-parser.vue71
-rw-r--r--src/tools/iban-validator-and-parser/index.ts12
-rw-r--r--src/tools/index.ts3
-rw-r--r--src/ui/c-key-value-list/c-key-value-list.types.ts9
-rw-r--r--src/ui/c-key-value-list/c-key-value-list.vue37
-rw-r--r--src/ui/c-text-copyable/c-text-copyable.demo.vue3
-rw-r--r--src/ui/c-text-copyable/c-text-copyable.vue17
-rw-r--r--src/ui/c-tooltip/c-tooltip.demo.vue17
-rw-r--r--src/ui/c-tooltip/c-tooltip.vue27
14 files changed, 278 insertions, 1 deletions
diff --git a/components.d.ts b/components.d.ts
index 076fc06..b88ae98 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 b68e04f..1cad277 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 9ceb775..0c2b5ca 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 0000000..3501543
--- /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 0000000..bde71db
--- /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 0000000..d5cdc02
--- /dev/null
+++ b/src/tools/iban-validator-and-parser/iban-validator-and-parser.vue
@@ -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>
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 0000000..b0cae50
--- /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 39b34ad..15770b5 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 0000000..40cc4ba
--- /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 0000000..e3b19af
--- /dev/null
+++ b/src/ui/c-key-value-list/c-key-value-list.vue
@@ -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>
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 0000000..e2aeb1d
--- /dev/null
+++ b/src/ui/c-text-copyable/c-text-copyable.demo.vue
@@ -0,0 +1,3 @@
+<template>
+ <c-text-copyable value="value" displayed-value="displayedValue" />
+</template>
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 0000000..b78e4cd
--- /dev/null
+++ b/src/ui/c-text-copyable/c-text-copyable.vue
@@ -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>
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 0000000..d385257
--- /dev/null
+++ b/src/ui/c-tooltip/c-tooltip.demo.vue
@@ -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>
diff --git a/src/ui/c-tooltip/c-tooltip.vue b/src/ui/c-tooltip/c-tooltip.vue
new file mode 100644
index 0000000..cc48fe1
--- /dev/null
+++ b/src/ui/c-tooltip/c-tooltip.vue
@@ -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>