feat(admin): responsive layout — bottom-tabs auf mobile, sidebar auf desktop
User-Wunsch: kleine screens (iPhone) keine sidebar, sondern bottom-tab-bar wie native rebreak-app. Layout-Architektur: - Desktop (lg+, ≥1024px): - Topbar: email + logout-button - Sidebar links (w-56) mit full-label-nav (versteckt <lg) - Content rechts (p-6) - Mobile (<lg): - Topbar: hamburger UDropdownMenu rechts (email + logout) - Sidebar versteckt - Content full-width (p-4 pb-24, damit content nicht hinter tab-bar) - Bottom-tab-bar: fixed bottom-0, border-t, bg-gray-950/95 backdrop-blur - 5 tabs in grid-cols-5: Home / Domains / Users / Stats / Mod - Icon (h-5 w-5) + label (text-[10px]) - Active-state: text-white bg-gray-800 (route-match isActive helper) - Safe-area-bottom respektiert via env(safe-area-inset-bottom) Pages-content unangetastet, nur layout. Build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
68fe8afab2
commit
e9d4434bf8
@ -1,13 +1,15 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-950 text-gray-100">
|
||||
<!-- Topbar -->
|
||||
<header class="border-b border-gray-800 bg-gray-900 px-6 py-3 flex items-center justify-between">
|
||||
<!-- Topbar (Desktop + Mobile) -->
|
||||
<header class="border-b border-gray-800 bg-gray-900 px-4 py-3 flex items-center justify-between lg:px-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold tracking-wide text-gray-400 uppercase">rebreak</span>
|
||||
<span class="text-gray-600">/</span>
|
||||
<span class="text-sm font-medium text-white">Admin</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
|
||||
<!-- Desktop: Email + Logout-Button sichtbar -->
|
||||
<div class="hidden lg:flex items-center gap-4">
|
||||
<span class="text-xs text-gray-500">{{ adminEmail }}</span>
|
||||
<UButton
|
||||
size="xs"
|
||||
@ -17,11 +19,24 @@
|
||||
@click="logout"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Mobile: Dropdown-Menu fuer Logout/Email -->
|
||||
<UDropdownMenu :items="mobileMenuItems" :popper="{ placement: 'bottom-end' }">
|
||||
<UButton
|
||||
icon="heroicons:bars-3"
|
||||
size="sm"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
class="lg:hidden"
|
||||
aria-label="Menu"
|
||||
/>
|
||||
</UDropdownMenu>
|
||||
</header>
|
||||
|
||||
<!-- Sidebar + Content -->
|
||||
<div class="flex">
|
||||
<aside class="w-56 min-h-[calc(100vh-49px)] border-r border-gray-800 bg-gray-900 py-4">
|
||||
<!-- Sidebar (Desktop) + Content -->
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<!-- Sidebar nur Desktop -->
|
||||
<aside class="hidden lg:block w-56 min-h-[calc(100vh-49px)] border-r border-gray-800 bg-gray-900 py-4">
|
||||
<nav class="flex flex-col gap-1 px-3">
|
||||
<NuxtLink
|
||||
v-for="item in nav"
|
||||
@ -36,21 +51,75 @@
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 p-6">
|
||||
<!-- Content -->
|
||||
<main class="flex-1 p-4 lg:p-6 pb-24 lg:pb-6">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Bottom-Tab-Bar (Mobile only) -->
|
||||
<nav
|
||||
class="lg:hidden fixed bottom-0 inset-x-0 z-40 border-t border-gray-800 bg-gray-950/95 backdrop-blur"
|
||||
:style="{ paddingBottom: 'env(safe-area-inset-bottom)' }"
|
||||
>
|
||||
<ul class="grid grid-cols-5">
|
||||
<li v-for="item in nav" :key="item.to">
|
||||
<NuxtLink
|
||||
:to="item.to"
|
||||
class="flex flex-col items-center justify-center gap-1 py-2 text-gray-500 transition-colors"
|
||||
:class="isActive(item.to)
|
||||
? 'text-white bg-gray-800'
|
||||
: 'hover:text-gray-300'"
|
||||
>
|
||||
<UIcon :name="item.icon" class="h-5 w-5" />
|
||||
<span class="text-[10px] font-medium leading-none">{{ item.shortLabel }}</span>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { adminEmail, logout } = useAdminAuth()
|
||||
const route = useRoute()
|
||||
|
||||
const nav = [
|
||||
{ to: "/", label: "Dashboard", icon: "heroicons:home" },
|
||||
{ to: "/domains", label: "Domain-Approval", icon: "heroicons:globe-alt" },
|
||||
{ to: "/users", label: "User-Management", icon: "heroicons:users" },
|
||||
{ to: "/stats", label: "Statistiken", icon: "heroicons:chart-bar" },
|
||||
{ to: "/moderation", label: "Moderation", icon: "heroicons:shield-check" },
|
||||
interface NavItem {
|
||||
to: string
|
||||
label: string
|
||||
shortLabel: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const nav: NavItem[] = [
|
||||
{ to: "/", label: "Dashboard", shortLabel: "Home", icon: "heroicons:home" },
|
||||
{ to: "/domains", label: "Domain-Approval", shortLabel: "Domains", icon: "heroicons:globe-alt" },
|
||||
{ to: "/users", label: "User-Management", shortLabel: "Users", icon: "heroicons:users" },
|
||||
{ to: "/stats", label: "Statistiken", shortLabel: "Stats", icon: "heroicons:chart-bar" },
|
||||
{ to: "/moderation", label: "Moderation", shortLabel: "Mod", icon: "heroicons:flag" },
|
||||
]
|
||||
|
||||
// Active-State: exact match fuer "/", startsWith fuer Sub-Routes
|
||||
function isActive(to: string): boolean {
|
||||
if (to === "/") return route.path === "/"
|
||||
return route.path === to || route.path.startsWith(`${to}/`)
|
||||
}
|
||||
|
||||
// Mobile-Dropdown-Menu (Header oben rechts)
|
||||
const mobileMenuItems = computed(() => [
|
||||
[
|
||||
{
|
||||
label: adminEmail.value || "Admin",
|
||||
icon: "heroicons:user-circle",
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
label: "Logout",
|
||||
icon: "heroicons:arrow-right-on-rectangle",
|
||||
onSelect: () => logout(),
|
||||
},
|
||||
],
|
||||
])
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user