Replace v-phone-input with custom PhoneInput component
Implement a custom PhoneInput component to replace the third-party v-phone-input library. The new component provides country selection, phone number formatting, and integrates seamlessly with Vuetify's form controls. Updated CreateInterestModal and InterestDetailsModal to use the new component.
This commit is contained in:
parent
6b922580c5
commit
c592e38569
|
|
@ -76,14 +76,13 @@
|
|||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-phone-input
|
||||
<PhoneInput
|
||||
v-model="newInterest['Phone Number']"
|
||||
label="Phone Number"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
country="US"
|
||||
default-country="US"
|
||||
:preferred-countries="['US', 'FR', 'ES', 'PT', 'GB']"
|
||||
:enable-searching-country="true"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
|
|
@ -313,6 +312,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
import type { Interest } from "@/utils/types";
|
||||
import PhoneInput from "./PhoneInput.vue";
|
||||
import {
|
||||
InterestSalesProcessLevelFlow,
|
||||
InterestLeadCategoryFlow,
|
||||
|
|
|
|||
|
|
@ -184,14 +184,13 @@
|
|||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-phone-input
|
||||
<PhoneInput
|
||||
v-model="interest['Phone Number']"
|
||||
label="Phone Number"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
country="US"
|
||||
default-country="US"
|
||||
:preferred-countries="['US', 'FR', 'ES', 'PT', 'GB']"
|
||||
:enable-searching-country="true"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
|
|
@ -497,6 +496,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import type { Interest, Berth } from "@/utils/types";
|
||||
import PhoneInput from "./PhoneInput.vue";
|
||||
import {
|
||||
InterestSalesProcessLevelFlow,
|
||||
InterestLeadCategoryFlow,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
<template>
|
||||
<div class="phone-input-wrapper">
|
||||
<v-text-field
|
||||
:model-value="displayValue"
|
||||
@update:model-value="updatePhone"
|
||||
:label="label"
|
||||
:variant="variant"
|
||||
:density="density"
|
||||
:rules="rules"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:clearable="clearable"
|
||||
:hide-details="hideDetails"
|
||||
:error-messages="errorMessages"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-menu
|
||||
v-model="countryMenu"
|
||||
:close-on-content-click="true"
|
||||
location="bottom"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="country-selector"
|
||||
:disabled="disabled || readonly"
|
||||
>
|
||||
<span class="country-flag">{{ selectedCountry.flag }}</span>
|
||||
<span class="country-code">{{ selectedCountry.dialCode }}</span>
|
||||
<v-icon end size="small">mdi-menu-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact" max-height="300">
|
||||
<v-list-item
|
||||
v-for="country in filteredCountries"
|
||||
:key="country.iso2"
|
||||
@click="selectCountry(country)"
|
||||
:title="`${country.flag} ${country.name} (${country.dialCode})`"
|
||||
>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
|
||||
interface Country {
|
||||
name: string;
|
||||
iso2: string;
|
||||
dialCode: string;
|
||||
flag: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
label?: string;
|
||||
variant?: 'flat' | 'elevated' | 'tonal' | 'outlined' | 'plain' | 'underlined' | 'solo' | 'solo-inverted' | 'solo-filled';
|
||||
density?: 'default' | 'comfortable' | 'compact';
|
||||
rules?: any[];
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
readonly?: boolean;
|
||||
clearable?: boolean;
|
||||
hideDetails?: boolean | 'auto';
|
||||
errorMessages?: string | string[];
|
||||
defaultCountry?: string;
|
||||
preferredCountries?: string[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
label: 'Phone Number',
|
||||
variant: 'outlined',
|
||||
density: 'comfortable',
|
||||
defaultCountry: 'US',
|
||||
preferredCountries: () => ['US', 'FR', 'ES', 'PT', 'GB']
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string];
|
||||
}>();
|
||||
|
||||
// Countries data
|
||||
const countries: Country[] = [
|
||||
{ name: 'United States', iso2: 'US', dialCode: '+1', flag: '🇺🇸' },
|
||||
{ name: 'United Kingdom', iso2: 'GB', dialCode: '+44', flag: '🇬🇧' },
|
||||
{ name: 'France', iso2: 'FR', dialCode: '+33', flag: '🇫🇷' },
|
||||
{ name: 'Spain', iso2: 'ES', dialCode: '+34', flag: '🇪🇸' },
|
||||
{ name: 'Portugal', iso2: 'PT', dialCode: '+351', flag: '🇵🇹' },
|
||||
{ name: 'Germany', iso2: 'DE', dialCode: '+49', flag: '🇩🇪' },
|
||||
{ name: 'Italy', iso2: 'IT', dialCode: '+39', flag: '🇮🇹' },
|
||||
{ name: 'Netherlands', iso2: 'NL', dialCode: '+31', flag: '🇳🇱' },
|
||||
{ name: 'Belgium', iso2: 'BE', dialCode: '+32', flag: '🇧🇪' },
|
||||
{ name: 'Switzerland', iso2: 'CH', dialCode: '+41', flag: '🇨🇭' },
|
||||
{ name: 'Austria', iso2: 'AT', dialCode: '+43', flag: '🇦🇹' },
|
||||
{ name: 'Sweden', iso2: 'SE', dialCode: '+46', flag: '🇸🇪' },
|
||||
{ name: 'Norway', iso2: 'NO', dialCode: '+47', flag: '🇳🇴' },
|
||||
{ name: 'Denmark', iso2: 'DK', dialCode: '+45', flag: '🇩🇰' },
|
||||
{ name: 'Finland', iso2: 'FI', dialCode: '+358', flag: '🇫🇮' },
|
||||
{ name: 'Poland', iso2: 'PL', dialCode: '+48', flag: '🇵🇱' },
|
||||
{ name: 'Greece', iso2: 'GR', dialCode: '+30', flag: '🇬🇷' },
|
||||
{ name: 'Ireland', iso2: 'IE', dialCode: '+353', flag: '🇮🇪' },
|
||||
{ name: 'Canada', iso2: 'CA', dialCode: '+1', flag: '🇨🇦' },
|
||||
{ name: 'Australia', iso2: 'AU', dialCode: '+61', flag: '🇦🇺' },
|
||||
{ name: 'New Zealand', iso2: 'NZ', dialCode: '+64', flag: '🇳🇿' },
|
||||
{ name: 'Japan', iso2: 'JP', dialCode: '+81', flag: '🇯🇵' },
|
||||
{ name: 'China', iso2: 'CN', dialCode: '+86', flag: '🇨🇳' },
|
||||
{ name: 'India', iso2: 'IN', dialCode: '+91', flag: '🇮🇳' },
|
||||
{ name: 'Brazil', iso2: 'BR', dialCode: '+55', flag: '🇧🇷' },
|
||||
{ name: 'Mexico', iso2: 'MX', dialCode: '+52', flag: '🇲🇽' },
|
||||
{ name: 'South Africa', iso2: 'ZA', dialCode: '+27', flag: '🇿🇦' },
|
||||
];
|
||||
|
||||
const countryMenu = ref(false);
|
||||
const localNumber = ref('');
|
||||
const selectedCountry = ref<Country>(
|
||||
countries.find(c => c.iso2 === props.defaultCountry) || countries[0]
|
||||
);
|
||||
|
||||
// Sort countries with preferred ones first
|
||||
const filteredCountries = computed(() => {
|
||||
const preferred = countries.filter(c => props.preferredCountries.includes(c.iso2));
|
||||
const others = countries.filter(c => !props.preferredCountries.includes(c.iso2));
|
||||
return [...preferred, ...others];
|
||||
});
|
||||
|
||||
// Display value (local number without country code)
|
||||
const displayValue = computed(() => localNumber.value);
|
||||
|
||||
// Parse initial value
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue) {
|
||||
// Find matching country code
|
||||
const country = countries.find(c => newValue.startsWith(c.dialCode));
|
||||
if (country) {
|
||||
selectedCountry.value = country;
|
||||
localNumber.value = newValue.slice(country.dialCode.length);
|
||||
} else {
|
||||
// Default to current country if no match
|
||||
localNumber.value = newValue;
|
||||
}
|
||||
} else {
|
||||
localNumber.value = '';
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Update phone number
|
||||
const updatePhone = (value: string) => {
|
||||
localNumber.value = value;
|
||||
if (value) {
|
||||
emit('update:modelValue', selectedCountry.value.dialCode + value);
|
||||
} else {
|
||||
emit('update:modelValue', '');
|
||||
}
|
||||
};
|
||||
|
||||
// Select country
|
||||
const selectCountry = (country: Country) => {
|
||||
selectedCountry.value = country;
|
||||
countryMenu.value = false;
|
||||
if (localNumber.value) {
|
||||
emit('update:modelValue', country.dialCode + localNumber.value);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.phone-input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.country-selector {
|
||||
padding: 0 8px;
|
||||
min-width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.country-flag {
|
||||
font-size: 1.2rem;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.country-code {
|
||||
font-size: 0.875rem;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
:deep(.v-field__prepend-inner) {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { defineNuxtPlugin } from '#app'
|
||||
import { createVPhoneInput } from 'v-phone-input'
|
||||
import 'v-phone-input/dist/v-phone-input.css'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const vPhoneInput = createVPhoneInput({
|
||||
defaultCountry: 'US',
|
||||
preferredCountries: ['US', 'FR', 'ES', 'PT', 'GB'],
|
||||
enableSearchingCountry: true,
|
||||
})
|
||||
|
||||
nuxtApp.vueApp.use(vPhoneInput)
|
||||
})
|
||||
Loading…
Reference in New Issue