feat(i18n): Többnyelvűség alapjainak beállítása és UI takarítás
This commit is contained in:
288
frontend/src/components/DashboardHeader.vue
Normal file
288
frontend/src/components/DashboardHeader.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<header
|
||||
class="sticky top-0 z-50 w-full border-b border-white/[0.06] bg-[#04151F]/80 backdrop-blur-2xl"
|
||||
>
|
||||
<div class="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
|
||||
<!-- ── Left: Logo + Brand (clickable, links to /) ─────────────── -->
|
||||
<router-link to="/dashboard" class="flex items-center gap-3 cursor-pointer transition-all duration-300 hover:opacity-80 hover:scale-[1.02]">
|
||||
<!-- Real brand logo with premium gradient -->
|
||||
<img
|
||||
src="@/assets/sf-brand-logo.svg"
|
||||
class="h-10 w-auto mr-3 drop-shadow-md"
|
||||
alt="ServiceFinder Logo"
|
||||
/>
|
||||
|
||||
<!-- Brand text (hidden on very small screens) -->
|
||||
<div class="hidden sm:block">
|
||||
<span class="text-base font-black tracking-widest text-white">SERVICE</span>
|
||||
<span class="text-base font-black tracking-widest text-[#418890]">FINDER</span>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<!-- ── Center/Left: Funkciók Menu ─────────────────────────────── -->
|
||||
<div class="relative flex items-center func-menu-container">
|
||||
<!-- Funkciók trigger button -->
|
||||
<button
|
||||
@click.stop="isFuncMenuOpen = !isFuncMenuOpen"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-xl text-sm font-medium text-white/70 transition-all duration-200 hover:bg-white/5 hover:text-white cursor-pointer"
|
||||
:aria-label="t('menu.features')"
|
||||
>
|
||||
<!-- Hamburger / grid icon -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline">{{ t('menu.features') }}</span>
|
||||
</button>
|
||||
|
||||
<!-- ── Funkciók Dropdown (Glassmorphism Bento) ──────────────── -->
|
||||
<Transition
|
||||
enter-active-class="transition duration-150 ease-out"
|
||||
enter-from-class="scale-95 opacity-0 -translate-y-2"
|
||||
enter-to-class="scale-100 opacity-100 translate-y-0"
|
||||
leave-active-class="transition duration-100 ease-in"
|
||||
leave-from-class="scale-100 opacity-100 translate-y-0"
|
||||
leave-to-class="scale-95 opacity-0 -translate-y-2"
|
||||
>
|
||||
<div
|
||||
v-if="isFuncMenuOpen"
|
||||
class="absolute left-0 top-full mt-2 w-64 z-50 rounded-xl border border-white/10 bg-[#04151F]/90 backdrop-blur-xl shadow-2xl shadow-black/40 overflow-hidden"
|
||||
>
|
||||
<div class="py-2">
|
||||
<!-- Járművek (Garázs) -->
|
||||
<button
|
||||
@click="isFuncMenuOpen = false"
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-sm text-white/80 transition-all duration-150 hover:bg-white/5 hover:text-white"
|
||||
>
|
||||
<svg class="w-5 h-5 text-[#418890]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
<span>{{ t('menu.garage') }} <span class="text-white/40">({{ t('menu.garage') }})</span></span>
|
||||
</button>
|
||||
|
||||
<!-- Szerviznaptár -->
|
||||
<button
|
||||
@click="isFuncMenuOpen = false"
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-sm text-white/80 transition-all duration-150 hover:bg-white/5 hover:text-white"
|
||||
>
|
||||
<svg class="w-5 h-5 text-[#418890]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>{{ t('menu.calendar') }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Pénzügyek és Költségek -->
|
||||
<button
|
||||
@click="isFuncMenuOpen = false"
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-sm text-white/80 transition-all duration-150 hover:bg-white/5 hover:text-white"
|
||||
>
|
||||
<svg class="w-5 h-5 text-[#418890]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>{{ t('menu.finance') }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Élő Diagnosztika -->
|
||||
<button
|
||||
@click="isFuncMenuOpen = false"
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-sm text-white/80 transition-all duration-150 hover:bg-white/5 hover:text-white"
|
||||
>
|
||||
<svg class="w-5 h-5 text-[#418890]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
<span>{{ t('menu.diagnostic') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="border-white/10 my-1" />
|
||||
|
||||
<!-- Kiemelt: + Új Jármű Hozzáadása -->
|
||||
<div class="py-2 px-3">
|
||||
<button
|
||||
@click="isFuncMenuOpen = false"
|
||||
class="flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-[#418890] to-[#70BC84] px-4 py-3 text-sm font-bold text-white shadow-lg transition-all duration-200 hover:shadow-[0_0_20px_rgba(0,229,160,0.4)] hover:-translate-y-0.5"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{{ t('menu.addVehicle') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- ── Spacer to push right items ─────────────────────────────── -->
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- ── Right: Welcome + User Dropdown ─────────────────────────── -->
|
||||
<div class="relative flex items-center gap-4">
|
||||
<!-- Welcome text -->
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium text-white/90">
|
||||
{{ t('header.welcome') }}, <span class="font-semibold text-[#70BC84]">{{ displayName }}</span>!
|
||||
</p>
|
||||
<p v-if="subtitle" class="text-[11px] text-white/40">{{ subtitle }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Avatar button — click to toggle dropdown -->
|
||||
<button
|
||||
@click="isUserMenuOpen = !isUserMenuOpen"
|
||||
class="flex h-9 w-9 items-center justify-center rounded-full bg-[#70BC84]/20 text-sm font-bold text-[#70BC84] ring-2 ring-white/10 transition-all duration-200 hover:ring-[#70BC84]/50 focus:outline-none focus:ring-[#70BC84]/50 cursor-pointer"
|
||||
:aria-label="t('header.userMenu')"
|
||||
>
|
||||
{{ initials }}
|
||||
</button>
|
||||
|
||||
<!-- ── Dropdown Menu (Glassmorphism) ──────────────────────────── -->
|
||||
<Transition
|
||||
enter-active-class="transition duration-150 ease-out"
|
||||
enter-from-class="scale-95 opacity-0 -translate-y-2"
|
||||
enter-to-class="scale-100 opacity-100 translate-y-0"
|
||||
leave-active-class="transition duration-100 ease-in"
|
||||
leave-from-class="scale-100 opacity-100 translate-y-0"
|
||||
leave-to-class="scale-95 opacity-0 -translate-y-2"
|
||||
>
|
||||
<div
|
||||
v-if="isUserMenuOpen"
|
||||
class="absolute right-0 top-full mt-2 w-56 z-50 rounded-xl border border-white/10 bg-[#04151F]/90 backdrop-blur-xl shadow-2xl shadow-black/40 overflow-hidden"
|
||||
>
|
||||
<!-- User info header -->
|
||||
<div class="px-4 py-3 border-b border-white/5">
|
||||
<p class="text-sm font-semibold text-white/90 truncate">{{ fullName }}</p>
|
||||
<p class="text-xs text-white/40 truncate mt-0.5">{{ email }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Menu items -->
|
||||
<div class="py-1">
|
||||
<!-- Profile settings -->
|
||||
<button
|
||||
@click="isUserMenuOpen = false"
|
||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-white/80 transition-all duration-150 hover:bg-white/5 hover:text-white"
|
||||
>
|
||||
<!-- User icon -->
|
||||
<svg
|
||||
class="h-4 w-4 text-white/40"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('header.profile') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="border-white/5 my-1" />
|
||||
|
||||
<!-- Logout -->
|
||||
<div class="py-1">
|
||||
<button
|
||||
@click="handleLogout"
|
||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-white/70 transition-all duration-150 hover:bg-white/5 hover:text-red-400"
|
||||
>
|
||||
<!-- Logout icon -->
|
||||
<svg
|
||||
class="h-4 w-4 text-white/40 group-hover:text-red-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('header.logout') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
firstName?: string
|
||||
subtitle?: string
|
||||
}>(),
|
||||
{
|
||||
firstName: '',
|
||||
subtitle: 'Irányítópult',
|
||||
},
|
||||
)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// ── Dropdown states ────────────────────────────────────────────────
|
||||
const isUserMenuOpen = ref(false)
|
||||
const isFuncMenuOpen = ref(false)
|
||||
|
||||
// ── Close Funkciók menu on outside click ───────────────────────────
|
||||
function handleOutsideClick(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
if (isFuncMenuOpen.value && !target.closest('.func-menu-container')) {
|
||||
isFuncMenuOpen.value = false
|
||||
}
|
||||
}
|
||||
onMounted(() => document.addEventListener('mousedown', handleOutsideClick))
|
||||
onUnmounted(() => document.removeEventListener('mousedown', handleOutsideClick))
|
||||
|
||||
// ── Computed display helpers ───────────────────────────────────────
|
||||
const displayName = computed(() => {
|
||||
if (props.firstName && props.firstName !== 'Vendég') return props.firstName
|
||||
// Fallback: use email prefix (before @)
|
||||
if (authStore.user?.email) {
|
||||
const emailPrefix = authStore.user.email.split('@')[0]
|
||||
return emailPrefix || 'Felhasználó'
|
||||
}
|
||||
return 'Felhasználó'
|
||||
})
|
||||
|
||||
const fullName = computed(() => {
|
||||
if (!authStore.user) return 'Felhasználó'
|
||||
const { first_name, last_name } = authStore.user
|
||||
if (first_name && last_name) return `${first_name} ${last_name}`
|
||||
if (first_name) return first_name
|
||||
if (last_name) return last_name
|
||||
return authStore.user.email || 'Felhasználó'
|
||||
})
|
||||
|
||||
const email = computed(() => {
|
||||
return authStore.user?.email || ''
|
||||
})
|
||||
|
||||
const initials = computed(() => {
|
||||
if (!authStore.user) return '?'
|
||||
const { first_name, last_name } = authStore.user
|
||||
if (first_name && last_name) {
|
||||
return (first_name[0] + last_name[0]).toUpperCase()
|
||||
}
|
||||
if (first_name) return first_name[0].toUpperCase()
|
||||
if (authStore.user.email) return authStore.user.email[0].toUpperCase()
|
||||
return '?'
|
||||
})
|
||||
|
||||
// ── Logout handler ─────────────────────────────────────────────────
|
||||
function handleLogout() {
|
||||
isUserMenuOpen.value = false
|
||||
authStore.logout()
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user