monacousa-portal/components/PhoneInputWrapper.vue

227 lines
5.6 KiB
Vue

<template>
<div class="phone-input-wrapper">
<v-label v-if="label" class="v-label mb-2">{{ label }}</v-label>
<div class="phone-input-container" :class="{ 'phone-input-container--error': hasError }">
<PhoneInput
v-model="phoneValue"
@update="handlePhoneUpdate"
:preferred-countries="['MC', 'FR', 'US', 'IT', 'CH', 'GB']"
:translations="{
phoneInput: {
placeholder: placeholder || 'Phone number'
}
}"
country-locale="en-US"
:auto-format="true"
:no-formatting-as-you-type="false"
class="custom-phone-input"
/>
</div>
<div v-if="errorMessage" class="error-message mt-1">
<span class="text-error text-caption">{{ errorMessage }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import PhoneInput from 'base-vue-phone-input';
interface Props {
modelValue?: string;
label?: string;
placeholder?: string;
error?: boolean;
errorMessage?: string;
required?: boolean;
disabled?: boolean;
}
interface Emits {
(e: 'update:modelValue', value: string): void;
(e: 'phone-data', data: any): void;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
placeholder: 'Phone number',
error: false,
required: false,
disabled: false
});
const emit = defineEmits<Emits>();
// Internal phone value
const phoneValue = ref(props.modelValue || '');
const phoneData = ref(null);
// Watch for external changes
watch(() => props.modelValue, (newValue) => {
if (newValue !== phoneValue.value) {
phoneValue.value = newValue || '';
}
});
// Handle phone input updates
const handlePhoneUpdate = (data: any) => {
phoneData.value = data;
// Emit the formatted international number or the raw input
const formattedPhone = data?.formatInternational || data?.e164 || phoneValue.value;
emit('update:modelValue', formattedPhone);
emit('phone-data', data);
};
// Watch phoneValue changes to emit updates
watch(phoneValue, (newValue) => {
if (!phoneData.value) {
// If no phone data yet, just emit the raw value
emit('update:modelValue', newValue);
}
});
const hasError = computed(() => {
return props.error || !!props.errorMessage;
});
</script>
<style scoped>
.phone-input-wrapper {
width: 100%;
}
.phone-input-container {
border: 2px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 4px;
transition: border-color 0.2s ease-in-out;
background: rgb(var(--v-theme-surface));
}
.phone-input-container:hover {
border-color: rgba(var(--v-theme-on-surface), 0.87);
}
.phone-input-container:focus-within {
border-color: rgb(var(--v-theme-primary));
border-width: 2px;
}
.phone-input-container--error {
border-color: rgb(var(--v-theme-error)) !important;
}
/* Style the phone input to match Vuetify */
.phone-input-container :deep(.phone-input) {
border: none !important;
outline: none !important;
background: transparent !important;
font-family: 'Roboto', sans-serif;
font-size: 16px;
padding: 12px 16px;
width: 100%;
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
.phone-input-container :deep(.phone-input input) {
border: none !important;
outline: none !important;
background: transparent !important;
padding: 0 !important;
margin: 0 !important;
font-family: inherit;
font-size: inherit;
color: inherit;
width: 100%;
}
/* Style the country selector */
.phone-input-container :deep(.country-selector) {
border: none !important;
background: transparent !important;
padding: 0 8px 0 0;
margin-right: 8px;
font-size: 16px;
}
.phone-input-container :deep(.country-selector .selected-country) {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.phone-input-container :deep(.country-selector .selected-country:hover) {
background-color: rgba(var(--v-theme-on-surface), 0.08);
}
/* Style the dropdown */
.phone-input-container :deep(.country-list) {
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 4px;
background: rgb(var(--v-theme-surface));
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
}
.phone-input-container :deep(.country-list .country-option) {
padding: 8px 12px;
cursor: pointer;
transition: background-color 0.2s ease;
border-bottom: 1px solid rgba(var(--v-border-color), 0.12);
}
.phone-input-container :deep(.country-list .country-option:hover) {
background-color: rgba(var(--v-theme-primary), 0.08);
}
.phone-input-container :deep(.country-list .country-option:last-child) {
border-bottom: none;
}
/* Error styling */
.error-message {
min-height: 20px;
}
.text-error {
color: rgb(var(--v-theme-error)) !important;
}
/* Monaco/France flag priority styling */
.phone-input-container :deep(.country-option[data-country-code="MC"]) {
background-color: rgba(var(--v-theme-primary), 0.04);
font-weight: 500;
}
.phone-input-container :deep(.country-option[data-country-code="FR"]) {
background-color: rgba(var(--v-theme-primary), 0.04);
font-weight: 500;
}
/* Dark theme support */
@media (prefers-color-scheme: dark) {
.phone-input-container {
background: rgb(var(--v-theme-surface));
border-color: rgba(var(--v-theme-outline), 0.38);
}
.phone-input-container :deep(.country-list) {
background: rgb(var(--v-theme-surface));
border-color: rgba(var(--v-theme-outline), 0.38);
}
}
/* Responsive adjustments */
@media (max-width: 600px) {
.phone-input-container :deep(.phone-input) {
font-size: 16px; /* Prevent zoom on iOS */
}
}
</style>