143 lines
6.1 KiB
Vue
143 lines
6.1 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
|
import { ChevronDown, Search, Check } from 'lucide-vue-next';
|
|
|
|
const props = defineProps<{
|
|
modelValue: string;
|
|
placeholder?: string;
|
|
error?: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits(['update:modelValue']);
|
|
|
|
const isOpen = ref(false);
|
|
const searchQuery = ref('');
|
|
const containerRef = ref<HTMLElement | null>(null);
|
|
|
|
// Full Country List with Codes
|
|
const countries = [
|
|
{ code: 'DE', name: 'Germany' }, { code: 'AT', name: 'Austria' }, { code: 'CH', name: 'Switzerland' },
|
|
{ code: 'US', name: 'United States' }, { code: 'GB', name: 'United Kingdom' }, { code: 'FR', name: 'France' },
|
|
{ code: 'IT', name: 'Italy' }, { code: 'ES', name: 'Spain' }, { code: 'NL', name: 'Netherlands' },
|
|
{ code: 'BE', name: 'Belgium' }, { code: 'PL', name: 'Poland' }, { code: 'CZ', name: 'Czech Republic' },
|
|
{ code: 'DK', name: 'Denmark' }, { code: 'SE', name: 'Sweden' }, { code: 'NO', name: 'Norway' },
|
|
{ code: 'FI', name: 'Finland' }, { code: 'PT', name: 'Portugal' }, { code: 'GR', name: 'Greece' },
|
|
{ code: 'TR', name: 'Turkey' }, { code: 'RU', name: 'Russia' }, { code: 'UA', name: 'Ukraine' },
|
|
{ code: 'CA', name: 'Canada' }, { code: 'AU', name: 'Australia' }, { code: 'JP', name: 'Japan' },
|
|
{ code: 'CN', name: 'China' }, { code: 'BR', name: 'Brazil' }, { code: 'MX', name: 'Mexico' },
|
|
{ code: 'AR', name: 'Argentina' }, { code: 'IN', name: 'India' }, { code: 'ZA', name: 'South Africa' },
|
|
{ code: 'AE', name: 'United Arab Emirates' }, { code: 'KR', name: 'South Korea' }, { code: 'SG', name: 'Singapore' }
|
|
];
|
|
|
|
const filteredCountries = computed(() => {
|
|
if (!searchQuery.value) return countries;
|
|
return countries.filter(c => c.name.toLowerCase().includes(searchQuery.value.toLowerCase()));
|
|
});
|
|
|
|
const selectedCountry = computed(() => {
|
|
return countries.find(c => c.code === props.modelValue);
|
|
});
|
|
|
|
const toggle = () => isOpen.value = !isOpen.value;
|
|
|
|
const select = (code: string) => {
|
|
emit('update:modelValue', code);
|
|
isOpen.value = false;
|
|
searchQuery.value = '';
|
|
};
|
|
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (containerRef.value && !containerRef.value.contains(e.target as Node)) {
|
|
isOpen.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(() => document.addEventListener('click', handleClickOutside));
|
|
onUnmounted(() => document.removeEventListener('click', handleClickOutside));
|
|
</script>
|
|
|
|
<template>
|
|
<div class="relative" ref="containerRef">
|
|
<!-- Trigger Button -->
|
|
<div
|
|
@click="toggle"
|
|
class="flex items-center justify-between h-10 w-full rounded-md border bg-[#0a0a0a] px-3 py-2 text-sm cursor-pointer transition-all duration-200"
|
|
:class="[
|
|
error ? 'border-red-500' : isOpen ? 'border-[#00f2ff] ring-1 ring-[#00f2ff]' : 'border-[#151515] hover:border-[#333]'
|
|
]"
|
|
>
|
|
<div class="flex items-center gap-2" v-if="selectedCountry">
|
|
<img :src="`https://flagcdn.com/w20/${selectedCountry.code.toLowerCase()}.png`" class="w-5 h-3.5 object-cover rounded-sm" />
|
|
<span class="text-white font-medium">{{ selectedCountry.name }}</span>
|
|
</div>
|
|
<span v-else class="text-[#888]">{{ placeholder || 'Select Country' }}</span>
|
|
<ChevronDown class="w-4 h-4 text-[#666] transition-transform duration-200" :class="{ 'rotate-180': isOpen }" />
|
|
</div>
|
|
|
|
<!-- Dropdown Menu -->
|
|
<transition name="dropdown">
|
|
<div v-if="isOpen" class="absolute z-50 mt-2 w-full rounded-md border border-[#151515] bg-[#0a0a0a] shadow-[0_10px_40px_rgba(0,0,0,0.8)] overflow-hidden">
|
|
<!-- Search -->
|
|
<div class="p-2 border-b border-[#151515]">
|
|
<div class="relative">
|
|
<Search class="absolute left-2 top-2.5 w-3.5 h-3.5 text-[#666]" />
|
|
<input
|
|
v-model="searchQuery"
|
|
type="text"
|
|
placeholder="Search..."
|
|
class="w-full bg-[#111] border border-[#222] rounded-md py-1.5 pl-8 pr-3 text-xs text-white focus:outline-none focus:border-[#00f2ff] transition-colors"
|
|
autofocus
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- List -->
|
|
<div class="max-h-60 overflow-y-auto custom-scrollbar">
|
|
<div
|
|
v-for="country in filteredCountries"
|
|
:key="country.code"
|
|
@click="select(country.code)"
|
|
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-[#151515] transition-colors group"
|
|
:class="{ 'bg-[#151515]': modelValue === country.code }"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<img :src="`https://flagcdn.com/w20/${country.code.toLowerCase()}.png`" class="w-5 h-3.5 object-cover rounded-sm opacity-80 group-hover:opacity-100 transition-opacity" />
|
|
<span class="text-sm text-[#ccc] group-hover:text-white transition-colors">{{ country.name }}</span>
|
|
</div>
|
|
<Check v-if="modelValue === country.code" class="w-3.5 h-3.5 text-[#00f2ff]" />
|
|
</div>
|
|
<div v-if="filteredCountries.length === 0" class="px-3 py-4 text-center text-xs text-[#666]">
|
|
No country found.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.dropdown-enter-active,
|
|
.dropdown-leave-active {
|
|
transition: all 0.2s ease;
|
|
}
|
|
.dropdown-enter-from,
|
|
.dropdown-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: #0a0a0a;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: #333;
|
|
border-radius: 2px;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: #555;
|
|
}
|
|
</style>
|