146 lines
4.3 KiB
Vue
146 lines
4.3 KiB
Vue
<template>
|
|
<div class="relative" ref="dropdownRef">
|
|
<!-- Trigger Button -->
|
|
<button
|
|
class="flex items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-lg transition bg-slate-700/50 hover:bg-slate-700 text-slate-300 hover:text-white border border-slate-600/50"
|
|
@click="dropdownOpen = !dropdownOpen"
|
|
>
|
|
<span class="text-base leading-none">{{ flagFor(currentLocale) }}</span>
|
|
<span class="hidden sm:inline">{{ labelFor(currentLocale) }}</span>
|
|
<svg
|
|
class="w-3.5 h-3.5 text-slate-400 transition-transform duration-200"
|
|
:class="{ 'rotate-180': dropdownOpen }"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Dropdown Menu -->
|
|
<div
|
|
v-if="dropdownOpen"
|
|
class="absolute right-0 mt-2 w-44 bg-slate-800 border border-slate-700 rounded-xl shadow-xl py-1 z-50"
|
|
>
|
|
<button
|
|
v-for="loc in availableLocales"
|
|
:key="loc.code"
|
|
class="flex items-center gap-3 w-full px-4 py-2.5 text-sm transition"
|
|
:class="
|
|
loc.code === currentLocale
|
|
? 'bg-indigo-600/20 text-indigo-300'
|
|
: 'text-slate-300 hover:bg-slate-700 hover:text-white'
|
|
"
|
|
@click="selectLocale(loc.code)"
|
|
>
|
|
<span class="text-lg leading-none">{{ flagFor(loc.code) }}</span>
|
|
<span>{{ loc.name }}</span>
|
|
<span v-if="loc.code === currentLocale" class="ml-auto text-indigo-400">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
const { locale, locales } = useI18n()
|
|
|
|
const dropdownOpen = ref(false)
|
|
const dropdownRef = ref<HTMLElement | null>(null)
|
|
|
|
// ── Locale helpers ──────────────────────────────────────────────
|
|
|
|
interface LocaleEntry {
|
|
code: string
|
|
name: string
|
|
[key: string]: unknown
|
|
}
|
|
|
|
const availableLocales = computed(() => locales.value as LocaleEntry[])
|
|
const currentLocale = computed(() => locale.value)
|
|
|
|
/** Map of language codes to flag emojis */
|
|
const flagMap: Record<string, string> = {
|
|
hu: '🇭🇺',
|
|
en: '🇬🇧',
|
|
}
|
|
|
|
/** Map of language codes to display labels (short) */
|
|
const labelMap: Record<string, string> = {
|
|
hu: 'Magyar',
|
|
en: 'English',
|
|
}
|
|
|
|
function flagFor(code: string): string {
|
|
return flagMap[code] || '🌐'
|
|
}
|
|
|
|
function labelFor(code: string): string {
|
|
return labelMap[code] || code.toUpperCase()
|
|
}
|
|
|
|
// ── Locale switching ────────────────────────────────────────────
|
|
|
|
function getAuthHeaders() {
|
|
const tokenCookie = useCookie('access_token')
|
|
const token = tokenCookie.value
|
|
return token ? { Authorization: `Bearer ${token}` } : {}
|
|
}
|
|
|
|
async function selectLocale(code: string) {
|
|
if (code === currentLocale.value) {
|
|
dropdownOpen.value = false
|
|
return
|
|
}
|
|
|
|
// 1. Update vue-i18n locale immediately
|
|
locale.value = code
|
|
|
|
// 2. Persist to localStorage
|
|
try {
|
|
localStorage.setItem('preferred_language', code)
|
|
} catch {
|
|
// localStorage may be unavailable
|
|
}
|
|
|
|
// 3. Persist to backend
|
|
try {
|
|
await $fetch('/api/v1/auth/me/language', {
|
|
method: 'PATCH',
|
|
headers: {
|
|
...getAuthHeaders(),
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: { preferred_language: code },
|
|
})
|
|
} catch (err) {
|
|
console.error('[LanguageSwitcher] Failed to persist language preference:', err)
|
|
}
|
|
|
|
dropdownOpen.value = false
|
|
}
|
|
|
|
// ── Close on outside click ──────────────────────────────────────
|
|
|
|
function handleClickOutside(e: MouseEvent) {
|
|
if (dropdownRef.value && !dropdownRef.value.contains(e.target as Node)) {
|
|
dropdownOpen.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('click', handleClickOutside)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('click', handleClickOutside)
|
|
})
|
|
</script>
|