monacousa-portal/components/MultipleNationalityInput.vue

300 lines
7.5 KiB
Vue

<template>
<div class="multiple-nationality-input">
<v-label v-if="label" class="v-label mb-2">{{ label }}</v-label>
<div class="nationality-list">
<div
v-for="(nationality, index) in nationalities"
:key="`nationality-${index}`"
class="nationality-item d-flex align-center gap-2 mb-2"
>
<v-select
v-model="nationalities[index]"
:items="countryOptions"
:label="`Nationality ${index + 1}`"
variant="outlined"
density="compact"
:error="hasError"
:error-messages="errorMessage"
@update:model-value="updateNationalities"
>
<template #selection="{ item }">
<div class="d-flex align-center gap-2">
<CountryFlag
:country-code="item.value"
:show-name="false"
size="small"
/>
<span>{{ item.title }}</span>
</div>
</template>
<template #item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps">
<template #prepend>
<CountryFlag
:country-code="item.raw.code"
:show-name="false"
size="small"
/>
</template>
<v-list-item-title>{{ item.raw.name }}</v-list-item-title>
</v-list-item>
</template>
</v-select>
<v-btn
v-if="nationalities.length > 1"
icon="mdi-delete"
size="small"
variant="text"
color="error"
@click="removeNationality(index)"
:title="`Remove ${getCountryName(nationality) || 'nationality'}`"
/>
</div>
</div>
<div class="nationality-actions mt-2">
<v-btn
variant="outlined"
color="primary"
size="small"
prepend-icon="mdi-plus"
@click="addNationality"
:disabled="disabled || nationalities.length >= maxNationalities"
>
Add Nationality
</v-btn>
<span v-if="nationalities.length >= maxNationalities" class="text-caption text-medium-emphasis ml-2">
Maximum {{ maxNationalities }} nationalities allowed
</span>
</div>
<!-- Preview of selected nationalities -->
<div v-if="nationalities.length > 0 && !hasEmptyNationality" class="nationality-preview mt-3">
<v-label class="text-caption mb-1">Selected Nationalities:</v-label>
<div class="d-flex flex-wrap gap-1">
<v-chip
v-for="nationality in validNationalities"
:key="nationality"
size="small"
variant="tonal"
color="primary"
>
<CountryFlag
:country-code="nationality"
:show-name="false"
size="small"
class="mr-1"
/>
{{ getCountryName(nationality) }}
</v-chip>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { getAllCountries, getCountryName } from '~/utils/countries';
interface Props {
modelValue?: string; // Comma-separated string like "FR,MC,US"
label?: string;
error?: boolean;
errorMessage?: string;
disabled?: boolean;
maxNationalities?: number;
required?: boolean;
}
interface Emits {
(e: 'update:modelValue', value: string): void;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
maxNationalities: 5,
error: false,
disabled: false,
required: false
});
const emit = defineEmits<Emits>();
// Parse initial nationalities from comma-separated string
const parseNationalities = (value: string): string[] => {
if (!value || value.trim() === '') return [''];
return value.split(',').map(n => n.trim()).filter(n => n.length > 0);
};
// Reactive nationalities array
const nationalities = ref<string[]>(parseNationalities(props.modelValue));
// Ensure there's always at least one empty nationality field
if (nationalities.value.length === 0) {
nationalities.value = [''];
}
// Watch for external model changes
watch(() => props.modelValue, (newValue) => {
const newNationalities = parseNationalities(newValue || '');
if (newNationalities.length === 0) newNationalities.push('');
// Only update if different to prevent loops
const current = nationalities.value.filter(n => n).join(',');
const incoming = newNationalities.filter(n => n).join(',');
if (current !== incoming) {
nationalities.value = newNationalities;
}
});
// Country options for dropdowns
const countryOptions = computed(() => {
const countries = getAllCountries();
return countries.map(country => ({
title: country.name,
value: country.code,
code: country.code,
name: country.name
}));
});
// Computed properties
const validNationalities = computed(() => {
return nationalities.value.filter(n => n && n.trim().length > 0);
});
const hasEmptyNationality = computed(() => {
return nationalities.value.some(n => !n || n.trim() === '');
});
const hasError = computed(() => {
return props.error || !!props.errorMessage;
});
// Methods
const addNationality = () => {
if (nationalities.value.length < props.maxNationalities) {
nationalities.value.push('');
}
};
const removeNationality = (index: number) => {
if (nationalities.value.length > 1) {
nationalities.value.splice(index, 1);
updateNationalities();
}
};
const updateNationalities = () => {
// Remove duplicates and empty values for the model
const uniqueValid = [...new Set(validNationalities.value)];
const result = uniqueValid.join(',');
emit('update:modelValue', result);
};
// Watch nationalities array for changes
watch(nationalities, () => {
updateNationalities();
}, { deep: true });
// Initialize the model value on mount if needed
onMounted(() => {
if (!props.modelValue && validNationalities.value.length > 0) {
updateNationalities();
}
});
</script>
<style scoped>
.multiple-nationality-input {
width: 100%;
}
.nationality-item {
position: relative;
}
.nationality-item .v-select {
flex: 1;
}
.nationality-actions {
display: flex;
align-items: center;
gap: 8px;
}
.nationality-preview {
padding: 12px;
background: rgba(var(--v-theme-surface-variant), 0.1);
border-radius: 8px;
border-left: 4px solid rgb(var(--v-theme-primary));
}
.nationality-preview .v-chip {
margin: 2px;
}
/* Animation for adding/removing items */
.nationality-item {
transition: all 0.3s ease;
}
.nationality-item.v-enter-active,
.nationality-item.v-leave-active {
transition: all 0.3s ease;
}
.nationality-item.v-enter-from,
.nationality-item.v-leave-to {
opacity: 0;
transform: translateY(-10px);
}
/* Error styling */
.error-message {
color: rgb(var(--v-theme-error));
font-size: 0.75rem;
margin-top: 4px;
}
/* Focus and hover states */
.nationality-item .v-btn:hover {
background-color: rgba(var(--v-theme-error), 0.08);
}
/* Responsive adjustments */
@media (max-width: 600px) {
.nationality-item {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.nationality-item .v-btn {
align-self: flex-end;
width: fit-content;
}
}
/* Priority countries styling in dropdowns */
:deep(.v-list-item[data-country="MC"]) {
background-color: rgba(var(--v-theme-primary), 0.04);
font-weight: 500;
}
:deep(.v-list-item[data-country="FR"]) {
background-color: rgba(var(--v-theme-primary), 0.04);
font-weight: 500;
}
:deep(.v-list-item[data-country="US"]) {
background-color: rgba(var(--v-theme-primary), 0.02);
}
</style>