feat(new tool): phone parser and normalizer

This commit is contained in:
Corentin Thomasset 2023-05-01 16:43:45 +02:00 committed by Corentin THOMASSET
parent 3f6c8f0edd
commit ce3150c65d
10 changed files with 357 additions and 140 deletions

View file

@ -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 phoneParserAndFormatter } from './phone-parser-and-formatter';
import { tool as jsonDiff } from './json-diff';
import { tool as ipv4RangeExpander } from './ipv4-range-expander';
import { tool as httpStatusCodes } from './http-status-codes';
@ -128,6 +129,10 @@ export const toolsByCategory: ToolCategory[] = [
name: 'Text',
components: [loremIpsumGenerator, textStatistics],
},
{
name: 'Data',
components: [phoneParserAndFormatter],
},
];
export const tools = toolsByCategory.flatMap(({ components }) => components);

View file

@ -0,0 +1,25 @@
import { Phone } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Phone parser and formatter',
path: '/phone-parser-and-formatter',
description:
'Parse, validate and format phone numbers. Get information about the phone number, like the country code, type, etc.',
keywords: [
'phone',
'parser',
'formatter',
'validate',
'format',
'number',
'telephone',
'mobile',
'cell',
'international',
'national',
],
component: () => import('./phone-parser-and-formatter.vue'),
icon: Phone,
createdAt: new Date('2023-05-01'),
});

View file

@ -0,0 +1,11 @@
import { test, expect } from '@playwright/test';
test.describe('Tool - Phone parser and formatter', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/phone-parser-and-formatter');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('Phone parser and formatter - IT Tools');
});
});

View file

@ -0,0 +1,41 @@
import type { NumberType } from 'libphonenumber-js/types';
import lookup from 'country-code-lookup';
export { formatTypeToHumanReadable, getFullCountryName, getDefaultCountryCode };
const typeToLabel: Record<NonNullable<NumberType>, string> = {
MOBILE: 'Mobile',
FIXED_LINE: 'Fixed line',
FIXED_LINE_OR_MOBILE: 'Fixed line or mobile',
PERSONAL_NUMBER: 'Personal number',
PREMIUM_RATE: 'Premium rate',
SHARED_COST: 'Shared cost',
TOLL_FREE: 'Toll free',
UAN: 'Universal access number',
VOICEMAIL: 'Voicemail',
VOIP: 'VoIP',
PAGER: 'Pager',
};
function formatTypeToHumanReadable(type: NumberType): string | undefined {
if (!type) return undefined;
return typeToLabel[type];
}
function getFullCountryName(countryCode: string | undefined) {
if (!countryCode) return undefined;
return lookup.byIso(countryCode)?.country;
}
function getDefaultCountryCode({
locale = window.navigator.language,
defaultCode = 'FR',
}: { locale?: string; defaultCode?: string } = {}): string {
const countryCode = locale.split('-')[1]?.toUpperCase();
if (!countryCode) return defaultCode;
return lookup.byIso(countryCode)?.iso2 ?? defaultCode;
}

View file

@ -0,0 +1,107 @@
<template>
<div>
<n-form-item label="Default country code:">
<n-select v-model:value="defaultCountryCode" :options="countriesOptions" />
</n-form-item>
<n-form-item label="Phone number:" v-bind="validation.attrs">
<n-input v-model:value="rawPhone" placeholder="Enter a phone number" />
</n-form-item>
<n-table v-if="parsedDetails">
<tbody>
<tr v-for="{ label, value } in parsedDetails" :key="label">
<td>
<n-text strong>{{ label }}</n-text>
</td>
<td>
<span-copyable v-if="value" :value="value"></span-copyable>
<n-text v-else depth="3" italic>Unknown</n-text>
</td>
</tr>
</tbody>
</n-table>
</div>
</template>
<script setup lang="ts">
import { withDefaultOnError } from '@/utils/defaults';
import { parsePhoneNumber, getCountries, getCountryCallingCode } from 'libphonenumber-js/max';
import { booleanToHumanReadable } from '@/utils/boolean';
import { useValidation } from '@/composable/validation';
import lookup from 'country-code-lookup';
import {
formatTypeToHumanReadable,
getFullCountryName,
getDefaultCountryCode,
} from './phone-parser-and-formatter.models';
const rawPhone = ref('');
const defaultCountryCode = ref(getDefaultCountryCode());
const validation = useValidation({
source: rawPhone,
rules: [
{
validator: (value) => value === '' || /^[0-9 +\-()]+$/.test(value),
message: 'Invalid phone number',
},
],
});
const parsedDetails = computed(() => {
if (!validation.isValid) return undefined;
const parsed = withDefaultOnError(() => parsePhoneNumber(rawPhone.value, 'FR'), undefined);
if (!parsed) return undefined;
return [
{
label: 'Country',
value: parsed.country,
},
{
label: 'Country',
value: getFullCountryName(parsed.country),
},
{
label: 'Country calling code',
value: parsed.countryCallingCode,
},
{
label: 'Is valid?',
value: booleanToHumanReadable(parsed.isValid()),
},
{
label: 'Is possible?',
value: booleanToHumanReadable(parsed.isPossible()),
},
{
label: 'Type',
value: formatTypeToHumanReadable(parsed.getType()),
},
{
label: 'International format',
value: parsed.formatInternational(),
},
{
label: 'National format',
value: parsed.formatNational(),
},
{
label: 'E.164 format',
value: parsed.format('E.164'),
},
{
label: 'RFC3966 format',
value: parsed.format('RFC3966'),
},
];
});
const countriesOptions = getCountries().map((code) => ({
label: `${lookup.byIso(code)?.country || code} (+${getCountryCallingCode(code)})`,
value: code,
}));
</script>
<style lang="less" scoped></style>

View file

@ -1,6 +1,6 @@
import _ from 'lodash';
import { describe, expect, it } from 'vitest';
import { isNotThrowing } from './boolean';
import { booleanToHumanReadable, isNotThrowing } from './boolean';
describe('boolean utils', () => {
describe('isNotThrowing', () => {
@ -13,4 +13,11 @@ describe('boolean utils', () => {
).to.eql(false);
});
});
describe('booleanToHumanReadable', () => {
it('should return "Yes" if the value is true and "No" otherwise', () => {
expect(booleanToHumanReadable(true)).to.eql('Yes');
expect(booleanToHumanReadable(false)).to.eql('No');
});
});
});

View file

@ -1,4 +1,4 @@
export { isNotThrowing };
export { isNotThrowing, booleanToHumanReadable };
function isNotThrowing(cb: () => unknown): boolean {
try {
@ -8,3 +8,7 @@ function isNotThrowing(cb: () => unknown): boolean {
return false;
}
}
function booleanToHumanReadable(value: boolean): string {
return value ? 'Yes' : 'No';
}