mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-24 16:56:14 -04:00
feat(new tool): phone parser and normalizer
This commit is contained in:
parent
3f6c8f0edd
commit
ce3150c65d
10 changed files with 357 additions and 140 deletions
|
@ -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);
|
||||
|
|
25
src/tools/phone-parser-and-formatter/index.ts
Normal file
25
src/tools/phone-parser-and-formatter/index.ts
Normal 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'),
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue