admin felület különválasztva
This commit is contained in:
356
frontend_admin/layouts/default.vue
Normal file
356
frontend_admin/layouts/default.vue
Normal file
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-slate-900 text-white flex">
|
||||
<!-- Sidebar Overlay (mobile) -->
|
||||
<div
|
||||
v-if="sidebarOpen"
|
||||
class="fixed inset-0 bg-black/50 z-20 lg:hidden"
|
||||
@click="sidebarOpen = false"
|
||||
/>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
:class="[
|
||||
'fixed lg:static inset-y-0 left-0 z-30 flex flex-col bg-slate-800 border-r border-slate-700 transition-all duration-300',
|
||||
sidebarOpen ? 'w-64 translate-x-0' : '-translate-x-full lg:translate-x-0 lg:w-20',
|
||||
]"
|
||||
>
|
||||
<!-- Logo Area -->
|
||||
<div class="flex items-center h-16 px-4 border-b border-slate-700">
|
||||
<div v-if="sidebarOpen || !sidebarCollapsed" class="flex items-center gap-3">
|
||||
<img src="/sf_logo.png" class="h-8 w-auto" alt="SF Logo" />
|
||||
<span class="text-lg font-bold text-white whitespace-nowrap">Admin Panel</span>
|
||||
</div>
|
||||
<div v-else class="mx-auto">
|
||||
<img src="/sf_logo.png" class="h-8 w-auto" alt="SF Logo" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collapse Toggle (desktop) -->
|
||||
<button
|
||||
class="hidden lg:flex items-center justify-center h-10 mx-2 mt-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-700 transition"
|
||||
@click="sidebarCollapsed = !sidebarCollapsed"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 transition-transform duration-300"
|
||||
:class="{ 'rotate-180': sidebarCollapsed }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex-1 px-2 py-4 space-y-2 overflow-y-auto">
|
||||
<template v-for="group in menuGroups" :key="group.title">
|
||||
<!-- Group Header (visible when expanded) -->
|
||||
<div
|
||||
v-if="!sidebarCollapsed || sidebarOpen"
|
||||
class="px-3 py-1 text-xs font-semibold uppercase tracking-wider text-slate-500"
|
||||
>
|
||||
{{ group.title }}
|
||||
</div>
|
||||
|
||||
<!-- Menu Items -->
|
||||
<template v-for="item in group.items" :key="item.label">
|
||||
<!-- Item with submenu -->
|
||||
<div v-if="item.children">
|
||||
<button
|
||||
class="flex items-center w-full gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition"
|
||||
:class="isGroupActive(item) ? 'bg-indigo-600/20 text-indigo-300' : 'text-slate-300 hover:bg-slate-700 hover:text-white'"
|
||||
@click="toggleGroup(item)"
|
||||
>
|
||||
<span class="flex-shrink-0 w-5 h-5" v-html="item.icon" />
|
||||
<span v-show="!sidebarCollapsed || sidebarOpen" class="flex-1 text-left whitespace-nowrap">{{ item.label }}</span>
|
||||
<!-- Chevron -->
|
||||
<svg
|
||||
v-show="!sidebarCollapsed || sidebarOpen"
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': expandedGroups.has(item.label) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Submenu -->
|
||||
<div
|
||||
v-show="expandedGroups.has(item.label) && (!sidebarCollapsed || sidebarOpen)"
|
||||
class="ml-2 mt-1 space-y-1 overflow-hidden transition-all duration-200"
|
||||
>
|
||||
<NuxtLink
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:to="child.path"
|
||||
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition"
|
||||
:class="isActive(child.path) ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:bg-slate-700 hover:text-white'"
|
||||
@click="sidebarOpen = false"
|
||||
>
|
||||
<span class="flex-shrink-0 w-4 h-4" v-html="child.icon" />
|
||||
<span class="whitespace-nowrap">{{ child.label }}</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plain link item -->
|
||||
<NuxtLink
|
||||
v-else
|
||||
:to="item.path"
|
||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition"
|
||||
:class="isActive(item.path) ? 'bg-indigo-600 text-white' : 'text-slate-300 hover:bg-slate-700 hover:text-white'"
|
||||
@click="sidebarOpen = false"
|
||||
>
|
||||
<span class="flex-shrink-0 w-5 h-5" v-html="item.icon" />
|
||||
<span v-show="!sidebarCollapsed || sidebarOpen" class="whitespace-nowrap">{{ item.label }}</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
<!-- Sidebar Footer -->
|
||||
<div class="p-4 border-t border-slate-700">
|
||||
<div v-if="!sidebarCollapsed || sidebarOpen" class="text-xs text-slate-500">
|
||||
ServiceFinder v2.0
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<!-- Topbar -->
|
||||
<header class="sticky top-0 z-10 bg-slate-800/95 backdrop-blur-sm border-b border-slate-700">
|
||||
<div class="flex items-center justify-between h-16 px-4 lg:px-6">
|
||||
<!-- Left: Hamburger + Page Title -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
class="lg:hidden p-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-700 transition"
|
||||
@click="sidebarOpen = true"
|
||||
>
|
||||
<svg 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="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="text-lg font-semibold text-white truncate">{{ pageTitle }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- Right: Avatar / Dropdown -->
|
||||
<div class="relative" ref="dropdownRef">
|
||||
<button
|
||||
class="flex items-center gap-2 p-1.5 rounded-lg hover:bg-slate-700 transition"
|
||||
@click="dropdownOpen = !dropdownOpen"
|
||||
>
|
||||
<div class="w-8 h-8 rounded-full bg-indigo-600 flex items-center justify-center text-sm font-bold text-white">
|
||||
{{ userInitials }}
|
||||
</div>
|
||||
<span class="hidden sm:block text-sm text-slate-300">{{ userEmail }}</span>
|
||||
<svg class="w-4 h-4 text-slate-400" 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-56 bg-slate-800 border border-slate-700 rounded-xl shadow-xl py-1 z-50"
|
||||
>
|
||||
<div class="px-4 py-3 border-b border-slate-700">
|
||||
<p class="text-sm font-medium text-white">{{ userEmail }}</p>
|
||||
<p class="text-xs text-slate-400 mt-0.5">Administrator</p>
|
||||
</div>
|
||||
<NuxtLink
|
||||
to="/profile"
|
||||
class="flex items-center gap-3 px-4 py-2.5 text-sm text-slate-300 hover:bg-slate-700 hover:text-white transition"
|
||||
@click="dropdownOpen = false"
|
||||
>
|
||||
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
Profile Settings
|
||||
</NuxtLink>
|
||||
<button
|
||||
class="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-red-400 hover:bg-slate-700 hover:text-red-300 transition"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<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="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>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Page Content -->
|
||||
<main class="flex-1 p-4 lg:p-6 overflow-auto">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Sidebar state
|
||||
const sidebarOpen = ref(false)
|
||||
const sidebarCollapsed = ref(false)
|
||||
|
||||
// Dropdown state
|
||||
const dropdownOpen = ref(false)
|
||||
const dropdownRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// Expanded submenu groups
|
||||
const expandedGroups = ref<Set<string>>(new Set(['Központ']))
|
||||
|
||||
// User info from auth store
|
||||
const userEmail = computed(() => authStore.userEmail || 'admin@servicefinder.hu')
|
||||
const userInitials = computed(() => {
|
||||
const email = authStore.userEmail
|
||||
if (!email) return 'AD'
|
||||
return email
|
||||
.split('@')[0]
|
||||
.split('.')
|
||||
.map(s => s[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2)
|
||||
})
|
||||
|
||||
// --- Menu Group Definitions ---
|
||||
interface MenuChild {
|
||||
path: string
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
label: string
|
||||
path?: string
|
||||
icon: string
|
||||
children?: MenuChild[]
|
||||
}
|
||||
|
||||
interface MenuGroup {
|
||||
title: string
|
||||
items: MenuItem[]
|
||||
}
|
||||
|
||||
const menuGroups: MenuGroup[] = [
|
||||
{
|
||||
title: 'Központ',
|
||||
items: [
|
||||
{
|
||||
path: '/',
|
||||
label: 'Dashboard',
|
||||
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="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Felhasználók & Partnerek',
|
||||
items: [
|
||||
{
|
||||
label: 'Garázsok',
|
||||
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="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>',
|
||||
children: [
|
||||
{
|
||||
path: '/garages',
|
||||
label: 'Garázsok listája',
|
||||
icon: '<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="M4 6h16M4 12h16M4 18h16" /></svg>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Felhasználók',
|
||||
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="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" /></svg>',
|
||||
children: [
|
||||
{
|
||||
path: '/users',
|
||||
label: 'Felhasználók listája',
|
||||
icon: '<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="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" /></svg>',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Rendszer',
|
||||
items: [
|
||||
{
|
||||
path: '/permissions',
|
||||
label: 'Jogosultságok',
|
||||
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="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>',
|
||||
},
|
||||
{
|
||||
label: 'Rendszernaplók',
|
||||
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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>',
|
||||
children: [
|
||||
{
|
||||
path: '/logs',
|
||||
label: 'Rendszernaplók',
|
||||
icon: '<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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// Page title based on route
|
||||
const pageTitle = computed(() => {
|
||||
if (route.path === '/') return 'Dashboard'
|
||||
if (route.path.startsWith('/permissions')) return 'Jogosultság Kezelés'
|
||||
if (route.path.startsWith('/garages')) return 'Garázsok'
|
||||
if (route.path.startsWith('/users')) return 'Felhasználók'
|
||||
if (route.path.startsWith('/logs')) return 'Rendszernaplók'
|
||||
return 'Admin Panel'
|
||||
})
|
||||
|
||||
function isActive(path: string) {
|
||||
if (path === '/') return route.path === '/'
|
||||
return route.path.startsWith(path)
|
||||
}
|
||||
|
||||
function isGroupActive(item: MenuItem): boolean {
|
||||
if (!item.children) return false
|
||||
return item.children.some(child => isActive(child.path))
|
||||
}
|
||||
|
||||
function toggleGroup(item: MenuItem) {
|
||||
if (expandedGroups.value.has(item.label)) {
|
||||
expandedGroups.value.delete(item.label)
|
||||
} else {
|
||||
expandedGroups.value.add(item.label)
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
authStore.logout()
|
||||
dropdownOpen.value = false
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// Close dropdown 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>
|
||||
Reference in New Issue
Block a user