fixed phone flags
Build And Push Image / docker (push) Successful in 2m59s
Details
Build And Push Image / docker (push) Successful in 2m59s
Details
This commit is contained in:
parent
65bda25c8f
commit
d2057cc878
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="demo-container">
|
<div class="demo-container">
|
||||||
<v-card class="pa-6" elevation="2">
|
<v-card class="pa-6" elevation="2">
|
||||||
<v-card-title class="text-h5 mb-4">
|
<v-card-title class="text-h5 mb-4">
|
||||||
🎨 New Professional Phone Input
|
📱 Perfect Phone Input - Like Screenshot 2
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
<v-row>
|
<v-row>
|
||||||
|
|
@ -11,34 +11,65 @@
|
||||||
v-model="phoneNumber"
|
v-model="phoneNumber"
|
||||||
label="Phone Number"
|
label="Phone Number"
|
||||||
placeholder="Enter your phone number"
|
placeholder="Enter your phone number"
|
||||||
help-text="Monaco 🇲🇨 and France 🇫🇷 are prioritized at the top"
|
help-text="Clean Vuetify design with compact flag dropdown"
|
||||||
@phone-data="handlePhoneData"
|
@phone-data="handlePhoneData"
|
||||||
@country-changed="handleCountryChange"
|
@country-changed="handleCountryChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
class="mt-4"
|
||||||
|
@click="testUSNumber"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
Test: (917) 932-4061
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="secondary"
|
||||||
|
class="mt-4 ml-2"
|
||||||
|
@click="testMonacoNumber"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
Test: Monaco +377
|
||||||
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-card variant="outlined" class="pa-4">
|
<v-card variant="outlined" class="pa-4">
|
||||||
<v-card-title class="text-subtitle-1">Live Data:</v-card-title>
|
<v-card-title class="text-subtitle-1">Live Phone Data:</v-card-title>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<p><strong>Phone:</strong> {{ phoneNumber }}</p>
|
<p><strong>Number:</strong> <code>{{ phoneNumber }}</code></p>
|
||||||
<p><strong>Valid:</strong> {{ phoneData?.isValid ? '✅' : '❌' }}</p>
|
<p><strong>Valid:</strong> {{ phoneData?.isValid ? '✅ Valid' : '❌ Invalid' }}</p>
|
||||||
<p><strong>Country:</strong> {{ phoneData?.country?.name }} {{ getCountryFlag(phoneData?.country?.iso2) }}</p>
|
<p><strong>Country:</strong> {{ phoneData?.country?.name }} ({{ phoneData?.country?.iso2 }})</p>
|
||||||
<p><strong>Format:</strong> International</p>
|
<p><strong>Dial Code:</strong> {{ phoneData?.country?.dialCode }}</p>
|
||||||
</div>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-alert type="success" variant="tonal" class="mt-4">
|
<v-alert type="success" variant="tonal" class="mt-6">
|
||||||
<template #title>✨ Key Features:</template>
|
<template #title>✅ Perfect Implementation:</template>
|
||||||
<ul class="mt-2">
|
<ul class="mt-2">
|
||||||
<li><strong>Compact Dropdown:</strong> Max 240px height, professional styling</li>
|
<li><strong>Clean Design:</strong> Exactly like your screenshot - Vuetify text field with flag inside</li>
|
||||||
<li><strong>Monaco Priority:</strong> 🇲🇨 Monaco and 🇫🇷 France at the top</li>
|
<li><strong>Compact Dropdown:</strong> Max 240px height, not oversized</li>
|
||||||
<li><strong>Real Flags:</strong> High-quality country flag icons</li>
|
<li><strong>Real Flags:</strong> High-quality country flags from flagcdn.com</li>
|
||||||
<li><strong>Auto-Validation:</strong> Built-in phone number validation</li>
|
<li><strong>Monaco Priority:</strong> 🇲🇨 Monaco and 🇫🇷 France appear first</li>
|
||||||
<li><strong>Smart Formatting:</strong> International format with country codes</li>
|
<li><strong>Smart Formatting:</strong> Uses libphonenumber-js for proper formatting</li>
|
||||||
<li><strong>Responsive:</strong> Mobile-optimized design</li>
|
<li><strong>Search Functionality:</strong> Type to find countries quickly</li>
|
||||||
|
</ul>
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-alert type="info" variant="tonal" class="mt-4">
|
||||||
|
<template #title>🎯 Design Features:</template>
|
||||||
|
<ul class="mt-2">
|
||||||
|
<li>Flag size: 24x18px (perfect for text field)</li>
|
||||||
|
<li>Dropdown: Compact with search bar</li>
|
||||||
|
<li>Input field: Full width for phone number</li>
|
||||||
|
<li>Validation: Real-time phone number validation</li>
|
||||||
|
<li>Responsive: Works perfectly on mobile</li>
|
||||||
</ul>
|
</ul>
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
@ -60,20 +91,12 @@ const handleCountryChange = (country: any) => {
|
||||||
console.log('Country changed:', country);
|
console.log('Country changed:', country);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCountryFlag = (iso2: string | undefined) => {
|
const testUSNumber = () => {
|
||||||
if (!iso2) return '';
|
phoneNumber.value = '+19179324061';
|
||||||
const flagMap: Record<string, string> = {
|
};
|
||||||
'MC': '🇲🇨',
|
|
||||||
'FR': '🇫🇷',
|
const testMonacoNumber = () => {
|
||||||
'US': '🇺🇸',
|
phoneNumber.value = '+37799123456';
|
||||||
'IT': '🇮🇹',
|
|
||||||
'CH': '🇨🇭',
|
|
||||||
'GB': '🇬🇧',
|
|
||||||
'DE': '🇩🇪',
|
|
||||||
'ES': '🇪🇸',
|
|
||||||
'CA': '🇨🇦'
|
|
||||||
};
|
|
||||||
return flagMap[iso2.toUpperCase()] || '';
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -83,4 +106,12 @@ const getCountryFlag = (iso2: string | undefined) => {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: rgba(var(--v-theme-primary), 0.1);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,114 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="premium-phone-wrapper">
|
<div class="phone-input-wrapper">
|
||||||
<!-- Label -->
|
<v-text-field
|
||||||
<v-label v-if="label" class="phone-label">
|
v-model="localNumber"
|
||||||
{{ label }}
|
:label="label"
|
||||||
<span v-if="required" class="text-error ml-1">*</span>
|
|
||||||
</v-label>
|
|
||||||
|
|
||||||
<!-- Vue Tel Input with Professional Styling -->
|
|
||||||
<div class="phone-input-container" :class="{
|
|
||||||
'phone-input-container--error': hasError,
|
|
||||||
'phone-input-container--disabled': disabled
|
|
||||||
}">
|
|
||||||
<vue-tel-input
|
|
||||||
v-model="phoneValue"
|
|
||||||
:preferred-countries="preferredCountries"
|
|
||||||
:only-countries="onlyCountries.length > 0 ? onlyCountries : undefined"
|
|
||||||
:ignored-countries="ignoredCountries"
|
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled"
|
:error="error"
|
||||||
|
:error-messages="errorMessage"
|
||||||
|
:hint="helpText"
|
||||||
|
:persistent-hint="!!helpText"
|
||||||
:required="required"
|
:required="required"
|
||||||
:valid-characters-only="true"
|
:disabled="disabled"
|
||||||
:auto-default-country="false"
|
variant="outlined"
|
||||||
:default-country="defaultCountry"
|
density="comfortable"
|
||||||
:input-options="inputOptions"
|
@input="handleInput"
|
||||||
:dropdown-options="dropdownOptions"
|
@blur="handleBlur"
|
||||||
mode="international"
|
|
||||||
@on-input="handlePhoneInput"
|
|
||||||
@country-changed="handleCountryChange"
|
|
||||||
@validate="handleValidation"
|
|
||||||
:wrapper-classes="wrapperClasses"
|
|
||||||
:input-classes="inputClasses"
|
|
||||||
class="vue-tel-input-styled"
|
|
||||||
>
|
>
|
||||||
<!-- Custom arrow icon slot -->
|
<template #prepend-inner>
|
||||||
<template #arrow-icon>
|
<!-- Country Selector -->
|
||||||
<v-icon size="16" class="dropdown-arrow">mdi-chevron-down</v-icon>
|
<v-menu
|
||||||
|
v-model="dropdownOpen"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
location="bottom"
|
||||||
|
offset="4"
|
||||||
|
max-height="300"
|
||||||
|
>
|
||||||
|
<template #activator="{ props: menuProps }">
|
||||||
|
<div
|
||||||
|
v-bind="menuProps"
|
||||||
|
class="country-selector"
|
||||||
|
:class="{ 'country-selector--open': dropdownOpen }"
|
||||||
|
@click="toggleDropdown"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="flagUrl"
|
||||||
|
:alt="`${selectedCountry.name} flag`"
|
||||||
|
class="country-flag"
|
||||||
|
@error="handleFlagError"
|
||||||
|
/>
|
||||||
|
<span class="country-code">{{ selectedCountry.dialCode }}</span>
|
||||||
|
<v-icon
|
||||||
|
size="16"
|
||||||
|
class="dropdown-icon"
|
||||||
|
:class="{ 'dropdown-icon--rotated': dropdownOpen }"
|
||||||
|
>
|
||||||
|
mdi-chevron-down
|
||||||
|
</v-icon>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</vue-tel-input>
|
|
||||||
|
<!-- Dropdown Content -->
|
||||||
|
<v-card class="country-dropdown" elevation="8">
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="search-container">
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchQuery"
|
||||||
|
placeholder="Search countries..."
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
hide-details
|
||||||
|
class="search-input"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error Message -->
|
<!-- Country List -->
|
||||||
<div v-if="errorMessage" class="error-message">
|
<v-list class="country-list" density="compact">
|
||||||
<v-icon size="16" color="error" class="mr-1">mdi-alert-circle-outline</v-icon>
|
<v-list-item
|
||||||
<span class="text-error text-caption">{{ errorMessage }}</span>
|
v-for="country in filteredCountries"
|
||||||
</div>
|
:key="country.iso2"
|
||||||
|
:class="{
|
||||||
|
'country-item': true,
|
||||||
|
'country-item--selected': country.iso2 === selectedCountry.iso2,
|
||||||
|
'country-item--preferred': isPreferredCountry(country.iso2)
|
||||||
|
}"
|
||||||
|
@click="selectCountry(country)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<img
|
||||||
|
:src="getCountryFlagUrl(country.iso2)"
|
||||||
|
:alt="`${country.name} flag`"
|
||||||
|
class="list-flag"
|
||||||
|
@error="handleFlagError"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Help Text -->
|
<v-list-item-title class="country-name">
|
||||||
<div v-else-if="helpText" class="help-text">
|
{{ country.name }}
|
||||||
<span class="text-medium-emphasis text-caption">{{ helpText }}</span>
|
</v-list-item-title>
|
||||||
</div>
|
|
||||||
|
<template #append>
|
||||||
|
<span class="dial-code">{{ country.dialCode }}</span>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { VueTelInput } from 'vue-tel-input';
|
import { parsePhoneNumber, AsYouType } from 'libphonenumber-js';
|
||||||
import 'vue-tel-input/vue-tel-input.css';
|
|
||||||
|
interface Country {
|
||||||
|
name: string;
|
||||||
|
iso2: string;
|
||||||
|
dialCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: string;
|
modelValue?: string;
|
||||||
|
|
@ -66,27 +120,13 @@ interface Props {
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
defaultCountry?: string;
|
defaultCountry?: string;
|
||||||
onlyCountries?: string[];
|
preferredCountries?: string[];
|
||||||
ignoredCountries?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PhoneObject {
|
|
||||||
number: string;
|
|
||||||
isValid: boolean;
|
|
||||||
country: {
|
|
||||||
name: string;
|
|
||||||
iso2: string;
|
|
||||||
dialCode: string;
|
|
||||||
priority: number;
|
|
||||||
areaCodes?: string[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'update:modelValue', value: string): void;
|
(e: 'update:modelValue', value: string): void;
|
||||||
(e: 'phone-data', data: PhoneObject): void;
|
(e: 'country-changed', country: Country): void;
|
||||||
(e: 'country-changed', country: any): void;
|
(e: 'phone-data', data: { number: string; isValid: boolean; country: Country }): void;
|
||||||
(e: 'validate', data: PhoneObject): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
@ -95,328 +135,322 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
error: false,
|
error: false,
|
||||||
required: false,
|
required: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
defaultCountry: 'MC', // Default to Monaco
|
defaultCountry: 'MC',
|
||||||
onlyCountries: () => [],
|
preferredCountries: () => ['MC', 'FR', 'US', 'IT', 'CH']
|
||||||
ignoredCountries: () => []
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
// Monaco and France prioritized countries
|
// Countries data - comprehensive list
|
||||||
const preferredCountries = ['MC', 'FR', 'US', 'IT', 'CH', 'GB', 'DE', 'ES', 'CA'];
|
const countries: Country[] = [
|
||||||
|
{ name: 'Monaco', iso2: 'MC', dialCode: '+377' },
|
||||||
|
{ name: 'France', iso2: 'FR', dialCode: '+33' },
|
||||||
|
{ name: 'United States', iso2: 'US', dialCode: '+1' },
|
||||||
|
{ name: 'Italy', iso2: 'IT', dialCode: '+39' },
|
||||||
|
{ name: 'Switzerland', iso2: 'CH', dialCode: '+41' },
|
||||||
|
{ name: 'United Kingdom', iso2: 'GB', dialCode: '+44' },
|
||||||
|
{ name: 'Germany', iso2: 'DE', dialCode: '+49' },
|
||||||
|
{ name: 'Spain', iso2: 'ES', dialCode: '+34' },
|
||||||
|
{ name: 'Canada', iso2: 'CA', dialCode: '+1' },
|
||||||
|
{ name: 'Australia', iso2: 'AU', dialCode: '+61' },
|
||||||
|
{ name: 'Netherlands', iso2: 'NL', dialCode: '+31' },
|
||||||
|
{ name: 'Belgium', iso2: 'BE', dialCode: '+32' },
|
||||||
|
{ name: 'Portugal', iso2: 'PT', dialCode: '+351' },
|
||||||
|
{ name: 'Austria', iso2: 'AT', dialCode: '+43' },
|
||||||
|
{ name: 'Sweden', iso2: 'SE', dialCode: '+46' },
|
||||||
|
{ name: 'Norway', iso2: 'NO', dialCode: '+47' },
|
||||||
|
{ name: 'Denmark', iso2: 'DK', dialCode: '+45' },
|
||||||
|
{ name: 'Finland', iso2: 'FI', dialCode: '+358' },
|
||||||
|
{ name: 'Ireland', iso2: 'IE', dialCode: '+353' },
|
||||||
|
{ name: 'Luxembourg', iso2: 'LU', dialCode: '+352' },
|
||||||
|
{ name: 'Japan', iso2: 'JP', dialCode: '+81' },
|
||||||
|
{ name: 'South Korea', iso2: 'KR', dialCode: '+82' },
|
||||||
|
{ name: 'Singapore', iso2: 'SG', dialCode: '+65' },
|
||||||
|
{ name: 'Hong Kong', iso2: 'HK', dialCode: '+852' },
|
||||||
|
{ name: 'New Zealand', iso2: 'NZ', dialCode: '+64' },
|
||||||
|
{ name: 'Brazil', iso2: 'BR', dialCode: '+55' },
|
||||||
|
{ name: 'Mexico', iso2: 'MX', dialCode: '+52' },
|
||||||
|
{ name: 'Argentina', iso2: 'AR', dialCode: '+54' },
|
||||||
|
{ name: 'Chile', iso2: 'CL', dialCode: '+56' },
|
||||||
|
{ name: 'South Africa', iso2: 'ZA', dialCode: '+27' }
|
||||||
|
];
|
||||||
|
|
||||||
// Internal phone value
|
// Reactive state
|
||||||
const phoneValue = ref(props.modelValue);
|
const dropdownOpen = ref(false);
|
||||||
|
const searchQuery = ref('');
|
||||||
// Input and dropdown configuration
|
const localNumber = ref('');
|
||||||
const inputOptions = {
|
const selectedCountry = ref<Country>(
|
||||||
placeholder: props.placeholder,
|
countries.find(c => c.iso2 === props.defaultCountry) || countries[0]
|
||||||
required: props.required,
|
);
|
||||||
disabled: props.disabled,
|
|
||||||
showDialCode: false,
|
|
||||||
tabindex: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
const dropdownOptions = {
|
|
||||||
showDialCode: true,
|
|
||||||
tabindex: 0,
|
|
||||||
disabledDialCode: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Custom CSS classes
|
|
||||||
const wrapperClasses = 'premium-tel-wrapper';
|
|
||||||
const inputClasses = 'premium-tel-input';
|
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const hasError = computed(() => {
|
const flagUrl = computed(() => getCountryFlagUrl(selectedCountry.value.iso2));
|
||||||
return props.error || !!props.errorMessage;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Watch for external model value changes
|
const filteredCountries = computed(() => {
|
||||||
watch(() => props.modelValue, (newValue) => {
|
if (!searchQuery.value) {
|
||||||
if (newValue !== phoneValue.value) {
|
// Show preferred countries first, then alphabetical
|
||||||
phoneValue.value = newValue || '';
|
const preferred = countries.filter(c => isPreferredCountry(c.iso2));
|
||||||
|
const others = countries.filter(c => !isPreferredCountry(c.iso2));
|
||||||
|
return [...preferred, ...others];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = searchQuery.value.toLowerCase();
|
||||||
|
return countries.filter(country =>
|
||||||
|
country.name.toLowerCase().includes(query) ||
|
||||||
|
country.dialCode.includes(query) ||
|
||||||
|
country.iso2.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch internal value changes
|
// Methods
|
||||||
watch(phoneValue, (newValue) => {
|
const getCountryFlagUrl = (iso2: string) => {
|
||||||
emit('update:modelValue', newValue || '');
|
return `https://flagcdn.com/24x18/${iso2.toLowerCase()}.png`;
|
||||||
});
|
|
||||||
|
|
||||||
// Event handlers
|
|
||||||
const handlePhoneInput = (phoneNumber: string, phoneObject: PhoneObject) => {
|
|
||||||
phoneValue.value = phoneNumber;
|
|
||||||
emit('update:modelValue', phoneNumber);
|
|
||||||
emit('phone-data', phoneObject);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCountryChange = (country: any) => {
|
const isPreferredCountry = (iso2: string) => {
|
||||||
|
return props.preferredCountries.includes(iso2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
if (!props.disabled) {
|
||||||
|
dropdownOpen.value = !dropdownOpen.value;
|
||||||
|
if (dropdownOpen.value) {
|
||||||
|
searchQuery.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectCountry = (country: Country) => {
|
||||||
|
selectedCountry.value = country;
|
||||||
|
dropdownOpen.value = false;
|
||||||
emit('country-changed', country);
|
emit('country-changed', country);
|
||||||
};
|
|
||||||
|
|
||||||
const handleValidation = (phoneObject: PhoneObject) => {
|
// Reformat existing number with new country
|
||||||
emit('validate', phoneObject);
|
if (localNumber.value) {
|
||||||
};
|
handleInput();
|
||||||
|
|
||||||
// Initialize with model value
|
|
||||||
onMounted(() => {
|
|
||||||
if (props.modelValue) {
|
|
||||||
phoneValue.value = props.modelValue;
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const handleInput = () => {
|
||||||
|
const rawInput = localNumber.value;
|
||||||
|
|
||||||
|
// Create full international number
|
||||||
|
const fullNumber = selectedCountry.value.dialCode + rawInput.replace(/\D/g, '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse and validate
|
||||||
|
const phoneNumber = parsePhoneNumber(fullNumber);
|
||||||
|
const isValid = phoneNumber?.isValid() || false;
|
||||||
|
|
||||||
|
// Format for display (national format)
|
||||||
|
if (phoneNumber && isValid) {
|
||||||
|
const formatter = new AsYouType(selectedCountry.value.iso2 as any);
|
||||||
|
const formatted = formatter.input(rawInput);
|
||||||
|
localNumber.value = formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit data
|
||||||
|
emit('update:modelValue', fullNumber);
|
||||||
|
emit('phone-data', {
|
||||||
|
number: fullNumber,
|
||||||
|
isValid,
|
||||||
|
country: selectedCountry.value
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Handle invalid numbers gracefully
|
||||||
|
emit('update:modelValue', fullNumber);
|
||||||
|
emit('phone-data', {
|
||||||
|
number: fullNumber,
|
||||||
|
isValid: false,
|
||||||
|
country: selectedCountry.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
// Additional formatting on blur if needed
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFlagError = (event: Event) => {
|
||||||
|
// Fallback to a default flag or hide image
|
||||||
|
const img = event.target as HTMLImageElement;
|
||||||
|
img.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize from modelValue
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (newValue && newValue !== selectedCountry.value.dialCode + localNumber.value.replace(/\D/g, '')) {
|
||||||
|
try {
|
||||||
|
const phoneNumber = parsePhoneNumber(newValue);
|
||||||
|
if (phoneNumber) {
|
||||||
|
// Find matching country
|
||||||
|
const matchingCountry = countries.find(c =>
|
||||||
|
c.dialCode === '+' + phoneNumber.countryCallingCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingCountry) {
|
||||||
|
selectedCountry.value = matchingCountry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set local number (national format)
|
||||||
|
localNumber.value = phoneNumber.formatNational().replace(phoneNumber.countryCallingCode, '').trim();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Handle invalid initial value
|
||||||
|
localNumber.value = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.premium-phone-wrapper {
|
.phone-input-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.phone-label {
|
.country-selector {
|
||||||
display: block;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phone-input-container {
|
|
||||||
position: relative;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phone-input-container--error :deep(.vue-tel-input) {
|
|
||||||
border-color: rgb(var(--v-theme-error)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phone-input-container--disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vue Tel Input Styling */
|
|
||||||
.vue-tel-input-styled :deep(.vue-tel-input) {
|
|
||||||
border: 2px solid rgba(var(--v-theme-outline), 0.38);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: rgb(var(--v-theme-surface));
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.vue-tel-input:hover) {
|
|
||||||
border-color: rgba(var(--v-theme-on-surface), 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.vue-tel-input:focus-within) {
|
|
||||||
border-color: rgb(var(--v-theme-primary));
|
|
||||||
box-shadow: 0 0 0 2px rgba(var(--v-theme-primary), 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Country Selection Styling */
|
|
||||||
.vue-tel-input-styled :deep(.country-selector) {
|
|
||||||
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.04) 0%, rgba(var(--v-theme-primary), 0.08) 100%);
|
|
||||||
border-right: 1px solid rgba(var(--v-theme-outline), 0.12);
|
|
||||||
padding: 14px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
min-width: 80px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-selector:hover) {
|
|
||||||
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.08) 0%, rgba(var(--v-theme-primary), 0.16) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-selector .iti-flag) {
|
|
||||||
width: 20px;
|
|
||||||
height: 15px;
|
|
||||||
border-radius: 2px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-selector .selection) {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-weight: 600;
|
padding: 4px 8px;
|
||||||
font-size: 0.875rem;
|
border-radius: 6px;
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input Field Styling */
|
|
||||||
.vue-tel-input-styled :deep(.vue-tel-input input) {
|
|
||||||
border: none !important;
|
|
||||||
outline: none !important;
|
|
||||||
padding: 14px 16px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
||||||
background: transparent;
|
|
||||||
flex: 1;
|
|
||||||
font-family: 'Roboto', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.vue-tel-input input::placeholder) {
|
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dropdown Styling - COMPACT AND BEAUTIFUL */
|
|
||||||
.vue-tel-input-styled :deep(.country-list) {
|
|
||||||
background: rgb(var(--v-theme-surface));
|
|
||||||
border: 1px solid rgba(var(--v-theme-outline), 0.24);
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
||||||
margin-top: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
max-height: 240px !important; /* COMPACT HEIGHT */
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(var(--v-theme-primary), 0.3) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-list::-webkit-scrollbar) {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-list::-webkit-scrollbar-track) {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-list::-webkit-scrollbar-thumb) {
|
|
||||||
background: rgba(var(--v-theme-primary), 0.3);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Country List Items - COMPACT AND CLEAN */
|
|
||||||
.vue-tel-input-styled :deep(.country-list li) {
|
|
||||||
padding: 10px 16px !important; /* COMPACT PADDING */
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.2s ease;
|
||||||
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.06);
|
background: rgba(var(--v-theme-surface), 1);
|
||||||
display: flex;
|
border: 1px solid transparent;
|
||||||
align-items: center;
|
margin-right: 8px;
|
||||||
gap: 12px;
|
|
||||||
font-size: 0.875rem !important; /* SMALLER FONT */
|
|
||||||
line-height: 1.2 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-list li:hover) {
|
.country-selector:hover {
|
||||||
background: rgba(var(--v-theme-primary), 0.08);
|
background: rgba(var(--v-theme-primary), 0.08);
|
||||||
transform: translateX(2px);
|
border-color: rgba(var(--v-theme-primary), 0.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-list li.highlighted) {
|
.country-selector--open {
|
||||||
background: rgba(var(--v-theme-primary), 0.12);
|
background: rgba(var(--v-theme-primary), 0.12);
|
||||||
font-weight: 600;
|
border-color: rgba(var(--v-theme-primary), 0.48);
|
||||||
color: rgb(var(--v-theme-primary));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-list li.preferred) {
|
.country-flag {
|
||||||
background: linear-gradient(90deg, rgba(var(--v-theme-primary), 0.04) 0%, transparent 100%);
|
width: 24px;
|
||||||
border-left: 3px solid rgb(var(--v-theme-primary));
|
height: 18px;
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-list li:last-child) {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Flag Styling in Dropdown */
|
|
||||||
.vue-tel-input-styled :deep(.country-list .iti-flag) {
|
|
||||||
width: 20px !important;
|
|
||||||
height: 15px !important;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||||
flex-shrink: 0;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Country Names and Codes */
|
.country-code {
|
||||||
.vue-tel-input-styled :deep(.country-list .country-name) {
|
font-size: 0.875rem;
|
||||||
flex: 1;
|
|
||||||
font-weight: 500;
|
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-list .dial-code) {
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.8125rem;
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
min-width: 32px;
|
||||||
font-family: 'Roboto Mono', monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Arrow Icon Styling */
|
.dropdown-icon {
|
||||||
.dropdown-arrow {
|
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-selector.open) .dropdown-arrow {
|
.dropdown-icon--rotated {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Error Message */
|
/* Dropdown Styling */
|
||||||
.error-message {
|
.country-dropdown {
|
||||||
display: flex;
|
min-width: 280px;
|
||||||
align-items: center;
|
max-width: 320px;
|
||||||
margin-top: 8px;
|
border-radius: 8px;
|
||||||
padding: 6px 12px;
|
overflow: hidden;
|
||||||
background: rgba(var(--v-theme-error), 0.08);
|
|
||||||
border-radius: 6px;
|
|
||||||
border-left: 3px solid rgb(var(--v-theme-error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Help Text */
|
.search-container {
|
||||||
.help-text {
|
padding: 12px;
|
||||||
margin-top: 6px;
|
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.12);
|
||||||
padding: 0 12px;
|
background: rgba(var(--v-theme-surface), 1);
|
||||||
opacity: 0.8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
.search-input :deep(.v-field) {
|
||||||
|
background: rgba(var(--v-theme-surface), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Country List */
|
||||||
|
.country-list {
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgba(var(--v-theme-surface), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-list::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-list::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-list::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(var(--v-theme-primary), 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-item {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-item:hover {
|
||||||
|
background: rgba(var(--v-theme-primary), 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-item--selected {
|
||||||
|
background: rgba(var(--v-theme-primary), 0.12) !important;
|
||||||
|
border-left-color: rgb(var(--v-theme-primary));
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-item--preferred {
|
||||||
|
background: rgba(var(--v-theme-primary), 0.04);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-flag {
|
||||||
|
width: 20px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-name {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dial-code {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.vue-tel-input-styled :deep(.country-selector) {
|
.country-dropdown {
|
||||||
min-width: 70px;
|
min-width: 260px;
|
||||||
padding: 12px 10px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.vue-tel-input input) {
|
.country-list {
|
||||||
padding: 12px 14px;
|
max-height: 200px;
|
||||||
font-size: 0.9375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-list) {
|
|
||||||
max-height: 200px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-tel-input-styled :deep(.country-list li) {
|
|
||||||
padding: 8px 14px !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark Theme Support */
|
/* Dark theme support */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.vue-tel-input-styled :deep(.country-list) {
|
.country-dropdown {
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* High Contrast Support */
|
|
||||||
@media (prefers-contrast: high) {
|
|
||||||
.vue-tel-input-styled :deep(.vue-tel-input) {
|
|
||||||
border-width: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Monaco and France Priority Visual Enhancement */
|
|
||||||
.vue-tel-input-styled :deep(.country-list li[data-country-code="mc"]),
|
|
||||||
.vue-tel-input-styled :deep(.country-list li[data-country-code="fr"]) {
|
|
||||||
background: linear-gradient(90deg, rgba(var(--v-theme-primary), 0.06) 0%, rgba(var(--v-theme-primary), 0.02) 100%);
|
|
||||||
border-left: 2px solid rgb(var(--v-theme-primary));
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
"flag-icons": "^7.5.0",
|
"flag-icons": "^7.5.0",
|
||||||
"formidable": "^3.5.4",
|
"formidable": "^3.5.4",
|
||||||
|
"libphonenumber-js": "^1.12.10",
|
||||||
"mime-types": "^3.0.1",
|
"mime-types": "^3.0.1",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
"motion-v": "^1.6.1",
|
"motion-v": "^1.6.1",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
"flag-icons": "^7.5.0",
|
"flag-icons": "^7.5.0",
|
||||||
"formidable": "^3.5.4",
|
"formidable": "^3.5.4",
|
||||||
|
"libphonenumber-js": "^1.12.10",
|
||||||
"mime-types": "^3.0.1",
|
"mime-types": "^3.0.1",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
"motion-v": "^1.6.1",
|
"motion-v": "^1.6.1",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue