add user privileges & add translations
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- Sidebar -->
|
||||
<aside class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200">
|
||||
<aside
|
||||
class="fixed inset-y-0 left-0 bg-white border-r border-gray-200 transition-all duration-300 z-20"
|
||||
:class="sidebarCollapsed ? 'w-16' : 'w-64'"
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center h-16 px-6 border-b border-gray-200">
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Logo / Toggle Button -->
|
||||
<div class="flex items-center h-16 px-4 border-b border-gray-200" :class="sidebarCollapsed ? 'justify-center' : 'justify-between'">
|
||||
<div v-if="!sidebarCollapsed" class="flex items-center space-x-2">
|
||||
<div class="w-8 h-8 bg-gradient-to-br from-primary-500 to-primary-700 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
@@ -13,82 +16,109 @@
|
||||
</div>
|
||||
<span class="text-xl font-bold text-gray-900">{{ settings.siteName }}</span>
|
||||
</div>
|
||||
<button
|
||||
@click="toggleSidebar"
|
||||
class="p-2 rounded-lg text-gray-500 hover:bg-gray-100 transition-colors"
|
||||
:class="sidebarCollapsed ? 'mx-auto' : ''"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path v-if="!sidebarCollapsed" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex-1 px-4 py-6 space-y-1 overflow-y-auto">
|
||||
<nav class="flex-1 px-2 py-6 space-y-1 overflow-y-auto">
|
||||
<router-link
|
||||
to="/dashboard"
|
||||
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors group"
|
||||
:class="{ 'bg-primary-50 text-primary-700': $route.path === '/dashboard' }"
|
||||
class="flex items-center rounded-lg hover:bg-gray-100 transition-colors group"
|
||||
:class="[
|
||||
$route.path === '/dashboard' ? 'bg-primary-50 text-primary-700' : 'text-gray-700',
|
||||
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3'
|
||||
]"
|
||||
:title="sidebarCollapsed ? t('app.dashboard') : ''"
|
||||
>
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5 shrink-0" 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>
|
||||
Dashboard
|
||||
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.dashboard') }}</span>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="userStore.role === 'admin'"
|
||||
to="/users"
|
||||
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
:class="{ 'bg-primary-50 text-primary-700': $route.path === '/users' }"
|
||||
class="flex items-center rounded-lg hover:bg-gray-100 transition-colors group"
|
||||
:class="[
|
||||
$route.path === '/users' ? 'bg-primary-50 text-primary-700' : 'text-gray-700',
|
||||
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3'
|
||||
]"
|
||||
:title="sidebarCollapsed ? t('app.users') : ''"
|
||||
>
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5 shrink-0" 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 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
Users
|
||||
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.users') }}</span>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
to="/restaurants"
|
||||
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
:class="{ 'bg-primary-50 text-primary-700': $route.path === '/restaurants' }"
|
||||
class="flex items-center rounded-lg hover:bg-gray-100 transition-colors group"
|
||||
:class="[
|
||||
$route.path === '/restaurants' ? 'bg-primary-50 text-primary-700' : 'text-gray-700',
|
||||
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3'
|
||||
]"
|
||||
:title="sidebarCollapsed ? t('app.restaurants') : ''"
|
||||
>
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5 shrink-0" 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>
|
||||
Restaurants
|
||||
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.restaurants') }}</span>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="userStore.role === 'admin'"
|
||||
to="/settings"
|
||||
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
:class="{ 'bg-primary-50 text-primary-700': $route.path === '/settings' }"
|
||||
class="flex items-center rounded-lg hover:bg-gray-100 transition-colors group"
|
||||
:class="[
|
||||
$route.path === '/settings' ? 'bg-primary-50 text-primary-700' : 'text-gray-700',
|
||||
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3'
|
||||
]"
|
||||
:title="sidebarCollapsed ? t('app.settings') : ''"
|
||||
>
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-5 h-5 shrink-0" 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.065 2.572c1.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.572 1.065c-.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.065-2.572c-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>
|
||||
Settings
|
||||
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.settings') }}</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<!-- User Profile -->
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
<!-- User Info (collapsed aware) -->
|
||||
<div v-if="!sidebarCollapsed" class="p-4 border-t border-gray-200">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-primary-500 to-primary-700 rounded-full flex items-center justify-center text-white font-semibold">
|
||||
{{ userInitials }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate">{{ userName }}</p>
|
||||
<p class="text-xs text-gray-500 truncate">Administrator</p>
|
||||
<p class="text-xs text-gray-500 truncate">{{ userStore.role === 'admin' ? 'Administrator' : 'User' }}</p>
|
||||
</div>
|
||||
<button @click="logout" class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<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="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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="p-2 border-t border-gray-200 flex justify-center">
|
||||
<div class="w-8 h-8 bg-gradient-to-br from-primary-500 to-primary-700 rounded-full flex items-center justify-center text-white font-semibold text-sm">
|
||||
{{ userInitials }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="ml-64">
|
||||
<!-- Header -->
|
||||
<header class="bg-white border-b border-gray-200">
|
||||
<div class="flex items-center justify-between h-16 px-8">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">{{ pageTitle }}</h1>
|
||||
<main class="transition-all duration-300" :class="sidebarCollapsed ? 'ml-16' : 'ml-64'">
|
||||
<!-- Header (без заголовка) -->
|
||||
<header class="bg-white border-b border-gray-200 sticky top-0 z-10">
|
||||
<div class="flex items-center justify-end h-16 px-8">
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Search -->
|
||||
<div class="relative">
|
||||
@@ -109,6 +139,25 @@
|
||||
</svg>
|
||||
<span class="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
|
||||
</button>
|
||||
|
||||
<!-- User actions -->
|
||||
<div class="flex items-center space-x-1 border-l pl-4 ml-2">
|
||||
<button @click="toggleLanguage" class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors" :title="t('app.language')">
|
||||
<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 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129" />
|
||||
</svg>
|
||||
</button>
|
||||
<router-link to="/profile" class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors" :title="t('app.profile')">
|
||||
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</router-link>
|
||||
<button @click="logout" class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors" :title="t('app.logout')">
|
||||
<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="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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -125,33 +174,47 @@
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useSettingsStore } from '../../stores/settings'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const settings = useSettingsStore()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userName = ref('Loading...')
|
||||
const userLogin = ref('')
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/me')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
userLogin.value = data.login
|
||||
userName.value = data.login // или можно сделать красивое отображение
|
||||
}
|
||||
} catch (e) {
|
||||
userName.value = 'User'
|
||||
const userName = computed(() => userStore.login || 'User')
|
||||
const userInitials = computed(() => (userName.value[0] || 'U').toUpperCase())
|
||||
|
||||
// Sidebar collapsed state with localStorage
|
||||
const SIDEBAR_STORAGE_KEY = 'admin_sidebar_collapsed'
|
||||
const sidebarCollapsed = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
const saved = localStorage.getItem(SIDEBAR_STORAGE_KEY)
|
||||
if (saved !== null) {
|
||||
sidebarCollapsed.value = saved === 'true'
|
||||
}
|
||||
})
|
||||
|
||||
const userInitials = computed(() => {
|
||||
return (userName.value[0] || 'U').toUpperCase()
|
||||
})
|
||||
function toggleSidebar() {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
localStorage.setItem(SIDEBAR_STORAGE_KEY, String(sidebarCollapsed.value))
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await fetch('/api/logout', { method: 'POST' })
|
||||
userStore.clear()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
async function toggleLanguage() {
|
||||
const newLang = locale.value === 'en' ? 'ru' : 'en'
|
||||
const ok = await userStore.updateProfile({ language: newLang })
|
||||
if (ok) {
|
||||
locale.value = newLang
|
||||
} else {
|
||||
locale.value = newLang
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user