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>
|
||||||
29
frontend/src/i18n/en.ts
Normal file
29
frontend/src/i18n/en.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export default {
|
||||||
|
menu: {
|
||||||
|
features: 'Features',
|
||||||
|
garage: 'Vehicles',
|
||||||
|
calendar: 'Service Calendar',
|
||||||
|
finance: 'Finance & Costs',
|
||||||
|
diagnostic: 'Live Diagnostics',
|
||||||
|
addVehicle: '+ Add New Vehicle',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
welcome: 'Welcome',
|
||||||
|
userMenu: 'User menu',
|
||||||
|
profile: 'Profile Settings',
|
||||||
|
logout: 'Logout',
|
||||||
|
subtitle: 'Your dashboard and vehicle overview',
|
||||||
|
},
|
||||||
|
dashboard: {
|
||||||
|
loading: 'Loading...',
|
||||||
|
errorRetry: 'Retry',
|
||||||
|
statusActive: 'Active',
|
||||||
|
statusUnknown: 'Unknown',
|
||||||
|
condition: 'Condition',
|
||||||
|
mileage: 'km',
|
||||||
|
},
|
||||||
|
zen: {
|
||||||
|
hide: 'Hide UI (Zen mode)',
|
||||||
|
show: 'Show UI',
|
||||||
|
},
|
||||||
|
}
|
||||||
29
frontend/src/i18n/hu.ts
Normal file
29
frontend/src/i18n/hu.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export default {
|
||||||
|
menu: {
|
||||||
|
features: 'Funkciók',
|
||||||
|
garage: 'Járművek',
|
||||||
|
calendar: 'Szerviznaptár',
|
||||||
|
finance: 'Pénzügyek és Költségek',
|
||||||
|
diagnostic: 'Élő Diagnosztika',
|
||||||
|
addVehicle: '+ Új Jármű Hozzáadása',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
welcome: 'Üdvözlünk',
|
||||||
|
userMenu: 'Felhasználói menü',
|
||||||
|
profile: 'Profil beállítások',
|
||||||
|
logout: 'Kilépés',
|
||||||
|
subtitle: 'Irányítópultod és járműveid áttekintése',
|
||||||
|
},
|
||||||
|
dashboard: {
|
||||||
|
loading: 'Betöltés...',
|
||||||
|
errorRetry: 'Újrapróbálkozás',
|
||||||
|
statusActive: 'Aktív',
|
||||||
|
statusUnknown: 'Ismeretlen',
|
||||||
|
condition: 'Állapot',
|
||||||
|
mileage: 'km',
|
||||||
|
},
|
||||||
|
zen: {
|
||||||
|
hide: 'UI elrejtése (Zen mód)',
|
||||||
|
show: 'UI megjelenítése',
|
||||||
|
},
|
||||||
|
}
|
||||||
28
frontend/src/main.ts
Normal file
28
frontend/src/main.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import hu from './i18n/hu'
|
||||||
|
import en from './i18n/en'
|
||||||
|
import './assets/main.css'
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
hu,
|
||||||
|
en,
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'hu',
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
messages,
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(i18n)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
266
frontend/src/views/DashboardView.vue
Normal file
266
frontend/src/views/DashboardView.vue
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative min-h-screen text-white">
|
||||||
|
<!-- Garage Background Image (no dark overlay — bright, light garage) -->
|
||||||
|
<div class="fixed inset-0 z-0">
|
||||||
|
<div class="absolute inset-0 bg-[url('@/assets/garage-bg.png')] bg-cover bg-center bg-fixed"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Spatial UI Layer: Hotspots (always visible, even in Zen mode) ── -->
|
||||||
|
<div class="fixed inset-0 z-[5] pointer-events-none">
|
||||||
|
<div class="relative w-full h-full">
|
||||||
|
<!-- Hotspot 1: Szervizkönyv/Naptár (bal fal) -->
|
||||||
|
<div class="absolute top-[30%] left-[20%] pointer-events-auto group">
|
||||||
|
<!-- Pulzáló hotspot kör -->
|
||||||
|
<div class="relative flex items-center justify-center cursor-pointer">
|
||||||
|
<div class="absolute w-10 h-10 rounded-full bg-[#FF3366]/20 animate-hotspot-ring"></div>
|
||||||
|
<div class="absolute w-10 h-10 rounded-full bg-[#FF3366]/20 animate-hotspot-ring-delayed"></div>
|
||||||
|
<div class="relative w-6 h-6 rounded-full bg-[#FF3366] shadow-[0_0_20px_#FF3366] animate-hotspot-pulse"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Glassmorphism Bento Box (hover opens) -->
|
||||||
|
<div class="absolute mt-4 w-64 opacity-0 pointer-events-none scale-95 transition-all duration-300 group-hover:opacity-100 group-hover:scale-100 group-hover:pointer-events-auto bg-[#04151F]/80 backdrop-blur-xl border border-white/20 rounded-xl p-4 shadow-2xl z-40">
|
||||||
|
<div class="flex items-center gap-3 mb-3">
|
||||||
|
<svg class="w-6 h-6 text-[#00E5A0]" 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>
|
||||||
|
<h3 class="text-lg font-bold text-white">Szerviznaptár</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-white/60">Közelgő események</p>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<div class="flex items-center justify-between text-xs text-white/40 border-b border-white/5 pb-1">
|
||||||
|
<span>Olajcsere</span>
|
||||||
|
<span class="text-[#00E5A0]">2 hét</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-xs text-white/40 border-b border-white/5 pb-1">
|
||||||
|
<span>Műszaki vizsga</span>
|
||||||
|
<span class="text-[#00E5A0]">3 hónap</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-xs text-white/40">
|
||||||
|
<span>Gumi szezonális</span>
|
||||||
|
<span class="text-yellow-400">1 hónap</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hotspot 2: Szerszámok/Beállítások (jobb fal) -->
|
||||||
|
<div class="absolute top-[30%] right-[20%] pointer-events-auto group">
|
||||||
|
<div class="relative flex items-center justify-center cursor-pointer">
|
||||||
|
<div class="absolute w-10 h-10 rounded-full bg-[#FF3366]/20 animate-hotspot-ring"></div>
|
||||||
|
<div class="absolute w-10 h-10 rounded-full bg-[#FF3366]/20 animate-hotspot-ring-delayed"></div>
|
||||||
|
<div class="relative w-6 h-6 rounded-full bg-[#FF3366] shadow-[0_0_20px_#FF3366] animate-hotspot-pulse"></div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute mt-4 w-64 opacity-0 pointer-events-none scale-95 transition-all duration-300 group-hover:opacity-100 group-hover:scale-100 group-hover:pointer-events-auto bg-[#04151F]/80 backdrop-blur-xl border border-white/20 rounded-xl p-4 shadow-2xl z-40">
|
||||||
|
<div class="flex items-center gap-3 mb-3">
|
||||||
|
<svg class="w-6 h-6 text-[#418890]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-lg font-bold text-white">Felszerelés</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-white/60">Alkatrészek és Skinek</p>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<div class="flex items-center justify-between text-xs text-white/40 border-b border-white/5 pb-1">
|
||||||
|
<span>Alkatrész katalógus</span>
|
||||||
|
<span class="text-[#418890]">42 tétel</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-xs text-white/40 border-b border-white/5 pb-1">
|
||||||
|
<span>Skin csomagok</span>
|
||||||
|
<span class="text-[#418890]">6 elérhető</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-xs text-white/40">
|
||||||
|
<span>Beállítások</span>
|
||||||
|
<span class="text-white/30">→</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hotspot 3: Diagnosztika (monitor) -->
|
||||||
|
<div class="absolute top-[60%] right-[30%] pointer-events-auto group">
|
||||||
|
<div class="relative flex items-center justify-center cursor-pointer">
|
||||||
|
<div class="absolute w-10 h-10 rounded-full bg-[#FF3366]/20 animate-hotspot-ring"></div>
|
||||||
|
<div class="absolute w-10 h-10 rounded-full bg-[#FF3366]/20 animate-hotspot-ring-delayed"></div>
|
||||||
|
<div class="relative w-6 h-6 rounded-full bg-[#FF3366] shadow-[0_0_20px_#FF3366] animate-hotspot-pulse"></div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute mt-4 w-64 opacity-0 pointer-events-none scale-95 transition-all duration-300 group-hover:opacity-100 group-hover:scale-100 group-hover:pointer-events-auto bg-[#04151F]/80 backdrop-blur-xl border border-white/20 rounded-xl p-4 shadow-2xl z-40">
|
||||||
|
<div class="flex items-center gap-3 mb-3">
|
||||||
|
<svg class="w-6 h-6 text-[#70BC84]" 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>
|
||||||
|
<h3 class="text-lg font-bold text-white">Élő Diagnosztika</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-white/60">Rendszerállapot</p>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<div class="flex items-center justify-between text-xs text-white/40 border-b border-white/5 pb-1">
|
||||||
|
<span>Motor állapot</span>
|
||||||
|
<span class="text-[#70BC84]">● Optimális</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-xs text-white/40 border-b border-white/5 pb-1">
|
||||||
|
<span>Akkumulátor</span>
|
||||||
|
<span class="text-[#70BC84]">● 87%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-xs text-white/40">
|
||||||
|
<span>Hibakódok</span>
|
||||||
|
<span class="text-white/30">0 találat</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Zen Mode FAB (always visible, above everything) ── -->
|
||||||
|
<button
|
||||||
|
@click="isUiVisible = !isUiVisible"
|
||||||
|
class="fixed bottom-8 right-8 z-50 btn-premium rounded-full p-4 shadow-2xl shadow-black/40"
|
||||||
|
:title="isUiVisible ? 'UI elrejtése (Zen mód)' : 'UI megjelenítése'"
|
||||||
|
>
|
||||||
|
<!-- Eye icon when visible, eye-off when hidden -->
|
||||||
|
<svg
|
||||||
|
v-if="isUiVisible"
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- ── Content (above the overlay) with fade transition ── -->
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-if="isUiVisible" class="relative z-10">
|
||||||
|
<!-- ── Top Navbar ──────────────────────────────────────────────── -->
|
||||||
|
<DashboardHeader
|
||||||
|
:first-name="authStore.user?.first_name || 'Vendég'"
|
||||||
|
subtitle="Irányítópultod és járműveid áttekintése"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
<!-- ── Loading State ──────────────────────────────────────────── -->
|
||||||
|
<div
|
||||||
|
v-if="vehicleStore.isLoading"
|
||||||
|
class="flex items-center justify-center py-20"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-10 w-10 animate-spin rounded-full border-4 border-white/10 border-t-[#70BC84]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Garage Grid ────────────────────────────────────────────── -->
|
||||||
|
<div
|
||||||
|
v-if="!vehicleStore.isLoading"
|
||||||
|
class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="vehicle in vehicleStore.vehicles"
|
||||||
|
:key="vehicle.id"
|
||||||
|
class="group relative overflow-hidden rounded-2xl border border-white/[0.08] bg-white/[0.05] p-6 backdrop-blur-xl shadow-xl shadow-black/20 transition-all duration-300 hover:border-[#70BC84]/30 hover:bg-white/[0.08] hover:shadow-2xl hover:shadow-black/30"
|
||||||
|
>
|
||||||
|
<!-- ── License Plate (EU-style) ──────────────────────────── -->
|
||||||
|
<div
|
||||||
|
v-if="vehicle.license_plate"
|
||||||
|
class="mb-4 inline-flex items-center overflow-hidden rounded-md border border-gray-600 shadow-sm"
|
||||||
|
>
|
||||||
|
<!-- Blue EU band -->
|
||||||
|
<div class="flex h-9 w-9 items-center justify-center bg-[#003399] text-[10px] font-bold leading-tight text-white">
|
||||||
|
<span class="block text-center leading-tight">SF</span>
|
||||||
|
</div>
|
||||||
|
<!-- White plate area -->
|
||||||
|
<div class="flex h-9 items-center bg-white px-3">
|
||||||
|
<span class="font-mono text-sm font-bold tracking-widest text-gray-900">
|
||||||
|
{{ vehicle.license_plate }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Brand + Model ─────────────────────────────────────── -->
|
||||||
|
<h3 class="text-xl font-bold text-white">
|
||||||
|
{{ vehicle.brand || 'Ismeretlen' }}
|
||||||
|
<span v-if="vehicle.model" class="font-normal text-white/70">{{ vehicle.model }}</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Year of manufacture -->
|
||||||
|
<p v-if="vehicle.year_of_manufacture" class="mt-0.5 text-sm text-white/40">
|
||||||
|
{{ vehicle.year_of_manufacture }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- ── Divider ────────────────────────────────────────────── -->
|
||||||
|
<div class="my-4 border-t border-white/[0.06]" />
|
||||||
|
|
||||||
|
<!-- ── Condition Score Bar ────────────────────────────────── -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="mb-1 flex items-center justify-between text-xs">
|
||||||
|
<span class="text-white/50">Állapot</span>
|
||||||
|
<span class="font-semibold text-[#70BC84]">{{ vehicle.condition_score }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-2 overflow-hidden rounded-full bg-white/10">
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full bg-gradient-to-r from-[#70BC84] to-[#008CA4] transition-all duration-500"
|
||||||
|
:style="{ width: Math.min(vehicle.condition_score, 100) + '%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Mileage ────────────────────────────────────────────── -->
|
||||||
|
<div class="flex items-center gap-2 text-sm text-white/60">
|
||||||
|
<svg class="h-4 w-4 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>{{ vehicle.current_mileage?.toLocaleString() || 0 }} km</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Status Badge ───────────────────────────────────────── -->
|
||||||
|
<div class="mt-4 flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
|
||||||
|
:class="vehicle.status === 'active'
|
||||||
|
? 'bg-[#70BC84]/15 text-[#70BC84]'
|
||||||
|
: 'bg-white/5 text-white/40'"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mr-1.5 inline-block h-1.5 w-1.5 rounded-full"
|
||||||
|
:class="vehicle.status === 'active' ? 'bg-[#70BC84]' : 'bg-white/20'"
|
||||||
|
/>
|
||||||
|
{{ vehicle.status === 'active' ? 'Aktív' : vehicle.status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- end relative z-10 content wrapper -->
|
||||||
|
</Transition>
|
||||||
|
</div><!-- end min-h-screen -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
import { useVehicleStore } from '../stores/vehicle'
|
||||||
|
import DashboardHeader from '../components/DashboardHeader.vue'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const vehicleStore = useVehicleStore()
|
||||||
|
|
||||||
|
// ── Zen Mode: UI láthatóság állapota ─────────────────────────────────
|
||||||
|
const isUiVisible = ref(true)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
vehicleStore.fetchVehicles()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user