vue · 8266 bytes Raw Blame History
1 <template>
2 <div class="app-layout min-h-screen bg-gray-50 dark:bg-gray-900">
3 <!-- Mobile sidebar backdrop -->
4 <div
5 v-if="isMobileSidebarOpen"
6 class="fixed inset-0 z-40 bg-gray-600 bg-opacity-75 lg:hidden"
7 @click="closeMobileSidebar"
8 ></div>
9
10 <!-- Sidebar -->
11 <div
12 class="sidebar fixed inset-y-0 left-0 z-50 w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transform lg:translate-x-0 lg:static lg:inset-0"
13 :class="{
14 'translate-x-0': isMobileSidebarOpen,
15 '-translate-x-full': !isMobileSidebarOpen
16 }"
17 >
18 <!-- Sidebar header -->
19 <div class="flex items-center justify-between h-16 px-4 border-b border-gray-200 dark:border-gray-700">
20 <h1 class="text-xl font-bold text-gray-900 dark:text-white">
21 ZephyrFS
22 </h1>
23 <button
24 @click="closeMobileSidebar"
25 class="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
26 >
27 <XMarkIcon class="w-5 h-5" />
28 </button>
29 </div>
30
31 <!-- Navigation -->
32 <nav class="flex-1 px-4 py-4 space-y-1">
33 <RouterLink
34 v-for="item in navigation"
35 :key="item.name"
36 :to="item.to"
37 class="nav-link group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors"
38 :class="[
39 $route.name === item.name || $route.path.startsWith(item.to)
40 ? 'bg-primary-100 dark:bg-primary-900/20 text-primary-700 dark:text-primary-300'
41 : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
42 ]"
43 @click="closeMobileSidebar"
44 >
45 <component
46 :is="item.icon"
47 class="w-5 h-5 mr-3"
48 :class="[
49 $route.name === item.name || $route.path.startsWith(item.to)
50 ? 'text-primary-600 dark:text-primary-400'
51 : 'text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300'
52 ]"
53 />
54 {{ item.label }}
55 </RouterLink>
56 </nav>
57
58 <!-- Network status -->
59 <div class="p-4 border-t border-gray-200 dark:border-gray-700">
60 <NetworkStatus />
61 </div>
62
63 <!-- User menu -->
64 <div class="p-4 border-t border-gray-200 dark:border-gray-700">
65 <Menu as="div" class="relative">
66 <MenuButton class="flex items-center w-full text-left">
67 <img
68 class="w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600"
69 :src="`https://ui-avatars.com/api/?name=${authStore.user?.username}&background=3b82f6&color=fff`"
70 :alt="authStore.user?.username"
71 />
72 <div class="ml-3 flex-1 min-w-0">
73 <p class="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
74 {{ authStore.user?.username }}
75 </p>
76 <p class="text-xs text-gray-500 dark:text-gray-400">
77 Online
78 </p>
79 </div>
80 <ChevronUpDownIcon class="w-4 h-4 text-gray-400" />
81 </MenuButton>
82
83 <MenuItems class="absolute bottom-full left-0 w-full mb-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg z-10">
84 <MenuItem v-slot="{ active }">
85 <RouterLink
86 to="/settings"
87 class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
88 :class="{ 'bg-gray-100 dark:bg-gray-700': active }"
89 @click="closeMobileSidebar"
90 >
91 Settings
92 </RouterLink>
93 </MenuItem>
94 <MenuItem v-slot="{ active }">
95 <button
96 @click="handleLogout"
97 class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
98 :class="{ 'bg-gray-100 dark:bg-gray-700': active }"
99 >
100 Sign out
101 </button>
102 </MenuItem>
103 </MenuItems>
104 </Menu>
105 </div>
106 </div>
107
108 <!-- Main content -->
109 <div class="lg:pl-64">
110 <!-- Top bar -->
111 <div class="sticky top-0 z-30 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 h-16">
112 <div class="flex items-center justify-between h-full px-4">
113 <!-- Mobile menu button -->
114 <button
115 @click="openMobileSidebar"
116 class="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
117 >
118 <Bars3Icon class="w-5 h-5" />
119 </button>
120
121 <!-- Page title -->
122 <div class="flex-1 lg:flex-none">
123 <h1 class="text-lg font-semibold text-gray-900 dark:text-white">
124 {{ pageTitle }}
125 </h1>
126 </div>
127
128 <!-- Top bar actions -->
129 <div class="flex items-center space-x-3">
130 <!-- Theme toggle -->
131 <button
132 @click="toggleTheme"
133 class="p-2 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
134 title="Toggle theme"
135 >
136 <SunIcon v-if="isDark" class="w-5 h-5" />
137 <MoonIcon v-else class="w-5 h-5" />
138 </button>
139
140 <!-- Notifications -->
141 <button
142 class="p-2 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 relative"
143 title="Notifications"
144 >
145 <BellIcon class="w-5 h-5" />
146 <span v-if="hasNotifications" class="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
147 </button>
148 </div>
149 </div>
150 </div>
151
152 <!-- Page content -->
153 <main class="flex-1">
154 <slot />
155 </main>
156 </div>
157 </div>
158 </template>
159
160 <script setup lang="ts">
161 import { ref, computed } from 'vue'
162 import { useRouter, useRoute } from 'vue-router'
163 import { useAuthStore } from '@/stores/auth'
164 import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
165 import {
166 Bars3Icon,
167 XMarkIcon,
168 ChevronUpDownIcon,
169 BellIcon,
170 SunIcon,
171 MoonIcon,
172 HomeIcon,
173 FolderIcon,
174 ArrowUpTrayIcon,
175 Cog6ToothIcon,
176 InformationCircleIcon,
177 } from '@heroicons/vue/24/outline'
178 import NetworkStatus from './NetworkStatus.vue'
179
180 // Store and router
181 const authStore = useAuthStore()
182 const router = useRouter()
183 const route = useRoute()
184
185 // State
186 const isMobileSidebarOpen = ref(false)
187 const isDark = ref(false)
188 const hasNotifications = ref(false)
189
190 // Navigation items
191 const navigation = [
192 { name: 'dashboard', label: 'Dashboard', to: '/', icon: HomeIcon },
193 { name: 'files', label: 'Files', to: '/files', icon: FolderIcon },
194 { name: 'upload', label: 'Upload', to: '/upload', icon: ArrowUpTrayIcon },
195 { name: 'settings', label: 'Settings', to: '/settings', icon: Cog6ToothIcon },
196 { name: 'about', label: 'About', to: '/about', icon: InformationCircleIcon },
197 ]
198
199 // Computed
200 const pageTitle = computed(() => {
201 const currentRoute = navigation.find(item =>
202 route.name === item.name || route.path.startsWith(item.to)
203 )
204 return currentRoute?.label || 'ZephyrFS'
205 })
206
207 // Methods
208 function openMobileSidebar() {
209 isMobileSidebarOpen.value = true
210 }
211
212 function closeMobileSidebar() {
213 isMobileSidebarOpen.value = false
214 }
215
216 function toggleTheme() {
217 isDark.value = !isDark.value
218 document.documentElement.classList.toggle('dark', isDark.value)
219 localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
220 }
221
222 async function handleLogout() {
223 try {
224 await authStore.logout()
225 router.push('/login')
226 } catch (error) {
227 console.error('Logout failed:', error)
228 }
229 }
230
231 // Initialize theme
232 const savedTheme = localStorage.getItem('theme')
233 if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
234 isDark.value = true
235 document.documentElement.classList.add('dark')
236 }
237 </script>
238
239 <style scoped>
240 .sidebar {
241 transition: transform 0.3s ease-in-out;
242 }
243
244 .nav-link {
245 @apply focus-ring;
246 }
247
248 @media (max-width: 1023px) {
249 .sidebar {
250 box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
251 }
252 }
253 </style>