| |
| |
| |
| |
|
|
| import { store } from '../state.js'; |
| import { router, Route } from '../router.js'; |
|
|
| export class Layout { |
| constructor(container) { |
| this.container = container; |
| this.unsubscribe = null; |
| } |
|
|
| render(contentComponent) { |
| const currentUser = store.state.currentUser; |
| const isSidebarOpen = store.state.ui.sidebarOpen; |
|
|
| this.container.innerHTML = ` |
| <div class="flex h-screen overflow-hidden bg-gray-50 dark:bg-slate-900"> |
| <!-- Sidebar --> |
| <aside id="sidebar" class="fixed inset-y-0 left-0 z-50 w-64 bg-white dark:bg-slate-800 border-r border-gray-200 dark:border-slate-700 transform transition-transform duration-300 ease-in-out ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'} lg:translate-x-0 lg:static lg:inset-0"> |
| <div class="flex flex-col h-full"> |
| <!-- Logo --> |
| <div class="flex items-center justify-between h-16 px-6 border-b border-gray-200 dark:border-slate-700"> |
| <a href="${Route.DASHBOARD}" data-route class="flex items-center space-x-2"> |
| <div class="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center"> |
| <i data-lucide="check-square" class="w-5 h-5 text-white"></i> |
| </div> |
| <span class="text-xl font-bold text-gray-900 dark:text-white">TaskFlow</span> |
| </a> |
| <button id="closeSidebar" class="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-500"> |
| <i data-lucide="x" class="w-5 h-5"></i> |
| </button> |
| </div> |
| |
| <!-- Navigation --> |
| <nav class="flex-1 px-4 py-6 space-y-1 overflow-y-auto custom-scrollbar"> |
| <div class="mb-6"> |
| <p class="px-2 text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Overview</p> |
| <a href="${Route.DASHBOARD}" data-route class="nav-item flex items-center px-2 py-2 text-sm font-medium rounded-md ${this.isActive(Route.DASHBOARD) ? 'bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300' : 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-700'}"> |
| <i data-lucide="layout-dashboard" class="mr-3 h-5 w-5"></i> |
| Dashboard |
| </a> |
| <a href="${Route.TASKS}" data-route class="nav-item flex items-center px-2 py-2 text-sm font-medium rounded-md ${this.isActive(Route.TASKS) ? 'bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300' : 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-700'}"> |
| <i data-lucide="list-todo" class="mr-3 h-5 w-5"></i> |
| Meine Tasks |
| ${this.getTaskBadge()} |
| </a> |
| <a href="${Route.CALENDAR}" data-route class="nav-item flex items-center px-2 py-2 text-sm font-medium rounded-md ${this.isActive(Route.CALENDAR) ? 'bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300' : 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-700'}"> |
| <i data-lucide="calendar" class="mr-3 h-5 w-5"></i> |
| Kalender |
| </a> |
| </div> |
| |
| <div> |
| <div class="flex items-center justify-between px-2 mb-2"> |
| <p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Projekte</p> |
| <button id="addProjectBtn" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"> |
| <i data-lucide="plus" class="w-4 h-4"></i> |
| </button> |
| </div> |
| <div id="projectList" class="space-y-1"> |
| ${this.renderProjectList()} |
| </div> |
| </div> |
| </nav> |
| |
| <!-- User Profile --> |
| <div class="border-t border-gray-200 dark:border-slate-700 p-4"> |
| <div class="flex items-center"> |
| <img class="h-8 w-8 rounded-full object-cover" src="${currentUser?.avatar || '/placeholder-avatar.png'}" alt="${currentUser?.name}"> |
| <div class="ml-3"> |
| <p class="text-sm font-medium text-gray-700 dark:text-gray-200">${currentUser?.name || 'Guest'}</p> |
| <p class="text-xs text-gray-500 dark:text-gray-400">${currentUser?.email || ''}</p> |
| </div> |
| <button id="userMenuBtn" class="ml-auto p-1 rounded-full hover:bg-gray-100 dark:hover:bg-slate-700"> |
| <i data-lucide="more-vertical" class="w-4 h-4 text-gray-400"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| </aside> |
| |
| <!-- Main Content --> |
| <div class="flex-1 flex flex-col overflow-hidden lg:ml-0"> |
| <!-- Header --> |
| <header class="flex items-center justify-between h-16 px-4 sm:px-6 lg:px-8 bg-white dark:bg-slate-800 border-b border-gray-200 dark:border-slate-700"> |
| <div class="flex items-center"> |
| <button id="toggleSidebar" class="p-2 rounded-md text-gray-400 hover:text-gray-500 lg:hidden"> |
| <i data-lucide="menu" class="w-6 h-6"></i> |
| </button> |
| <button id="toggleSidebarDesktop" class="hidden lg:block p-2 rounded-md text-gray-400 hover:text-gray-500 mr-2"> |
| <i data-lucide="panel-left" class="w-5 h-5"></i> |
| </button> |
| <h2 id="pageTitle" class="text-lg font-semibold text-gray-900 dark:text-white ml-2">Dashboard</h2> |
| </div> |
| |
| <div class="flex items-center space-x-4"> |
| <!-- Search --> |
| <div class="relative hidden sm:block"> |
| <i data-lucide="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4"></i> |
| <input type="text" id="globalSearch" placeholder="Suchen..." |
| class="w-64 pl-10 pr-4 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-gray-50 dark:bg-slate-700 text-sm focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:text-white" |
| value="${store.state.ui.searchQuery}"> |
| </div> |
| |
| <!-- Dark Mode Toggle --> |
| <button id="toggleDarkMode" class="p-2 rounded-lg text-gray-400 hover:text-gray-500 hover:bg-gray-100 dark:hover:bg-slate-700"> |
| <i data-lucide="${store.state.ui.darkMode ? 'sun' : 'moon'}" class="w-5 h-5"></i> |
| </button> |
| |
| <!-- Notifications --> |
| <button class="relative p-2 rounded-lg text-gray-400 hover:text-gray-500 hover:bg-gray-100 dark:hover:bg-slate-700"> |
| <i data-lucide="bell" class="w-5 h-5"></i> |
| ${store.getOverdueTasks().length > 0 ? ` |
| <span class="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span> |
| ` : ''} |
| </button> |
| </div> |
| </header> |
| |
| <!-- Page Content --> |
| <main id="mainContent" class="flex-1 overflow-y-auto bg-gray-50 dark:bg-slate-900 p-4 sm:p-6 lg:p-8"> |
| <!-- Content wird hier injected --> |
| </main> |
| </div> |
| |
| <!-- Mobile Sidebar Overlay --> |
| <div id="sidebarOverlay" class="fixed inset-0 z-40 bg-black bg-opacity-50 lg:hidden ${isSidebarOpen ? 'block' : 'hidden'}"></div> |
| </div> |
| `; |
|
|
| |
| lucide.createIcons(); |
|
|
| |
| this.bindEvents(); |
|
|
| |
| const mainContent = this.container.querySelector('#mainContent'); |
| if (contentComponent && mainContent) { |
| contentComponent.render(mainContent); |
| } |
|
|
| |
| this.unsubscribe = store.subscribe('*', () => { |
| this.updateUI(); |
| }); |
| } |
|
|
| renderProjectList() { |
| return store.state.projects.map(project => ` |
| <a href="${Route.PROJECT}?id=${project.id}" data-route |
| class="group flex items-center px-2 py-2 text-sm font-medium rounded-md ${this.isProjectActive(project.id) ? 'bg-gray-100 dark:bg-slate-700 text-gray-900 dark:text-white' : 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-600'}"> |
| <span class="w-2.5 h-2.5 rounded-full mr-3" style="background-color: ${project.color}"></span> |
| <span class="flex-1 truncate">${project.name}</span> |
| ${this.getProjectTaskCount(project.id) > 0 ? ` |
| <span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-slate-600 text-gray-600 dark:text-gray-300"> |
| ${this.getProjectTaskCount(project.id)} |
| </span> |
| ` : ''} |
| </a> |
| `).join(''); |
| } |
|
|
| getTaskBadge() { |
| const count = store.state.tasks.filter(t => t.assigneeId === store.state.currentUser?.id && t.status !== 'done').length; |
| if (count === 0) return ''; |
| return `<span class="ml-auto inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200">${count}</span>`; |
| } |
|
|
| getProjectTaskCount(projectId) { |
| return store.state.tasks.filter(t => t.projectId === projectId && t.status !== 'done').length; |
| } |
|
|
| isActive(route) { |
| return window.location.pathname === route; |
| } |
|
|
| isProjectActive(projectId) { |
| return window.location.pathname === Route.PROJECT && store.state.ui.selectedProjectId === projectId; |
| } |
|
|
| bindEvents() { |
| |
| const toggleBtn = this.container.querySelector('#toggleSidebar'); |
| const toggleDesktopBtn = this.container.querySelector('#toggleSidebarDesktop'); |
| const closeBtn = this.container.querySelector('#closeSidebar'); |
| const overlay = this.container.querySelector('#sidebarOverlay'); |
|
|
| [toggleBtn, toggleDesktopBtn].forEach(btn => { |
| btn?.addEventListener('click', () => { |
| const isOpen = !store.state.ui.sidebarOpen; |
| store.setUISState('sidebarOpen', isOpen); |
| this.updateSidebarVisibility(isOpen); |
| }); |
| }); |
|
|
| closeBtn?.addEventListener('click', () => { |
| store.setUISState('sidebarOpen', false); |
| this.updateSidebarVisibility(false); |
| }); |
|
|
| overlay?.addEventListener('click', () => { |
| store.setUISState('sidebarOpen', false); |
| this.updateSidebarVisibility(false); |
| }); |
|
|
| |
| this.container.querySelector('#toggleDarkMode')?.addEventListener('click', () => { |
| store.toggleDarkMode(); |
| }); |
|
|
| |
| const searchInput = this.container.querySelector('#globalSearch'); |
| searchInput?.addEventListener('input', (e) => { |
| store.setUISState('searchQuery', e.target.value); |
| }); |
|
|
| |
| this.container.querySelector('#addProjectBtn')?.addEventListener('click', () => { |
| this.showAddProjectModal(); |
| }); |
| } |
|
|
| updateSidebarVisibility(isOpen) { |
| const sidebar = this.container.querySelector('#sidebar'); |
| const overlay = this.container.querySelector('#sidebarOverlay'); |
| |
| if (isOpen) { |
| sidebar.classList.remove('-translate-x-full'); |
| overlay.classList.remove('hidden'); |
| overlay.classList.add('block'); |
| } else { |
| sidebar.classList.add('-translate-x-full'); |
| overlay.classList.add('hidden'); |
| overlay.classList.remove('block'); |
| } |
| } |
|
|
| updateUI() { |
| |
| const projectList = this.container.querySelector('#projectList'); |
| if (projectList) { |
| projectList.innerHTML = this.renderProjectList(); |
| } |
|
|
| |
| const searchInput = this.container.querySelector('#globalSearch'); |
| if (searchInput && searchInput.value !== store.state.ui.searchQuery) { |
| searchInput.value = store.state.ui.searchQuery; |
| } |
|
|
| |
| lucide.createIcons(); |
| } |
|
|
| showAddProjectModal() { |
| |
| const name = prompt('Projektname:'); |
| if (name) { |
| const description = prompt('Beschreibung (optional):') || ''; |
| const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899']; |
| const color = colors[Math.floor(Math.random() * colors.length)]; |
| |
| store.addProject({ |
| name, |
| description, |
| color, |
| ownerId: store.state.currentUser.id, |
| memberIds: [store.state.currentUser.id] |
| }); |
| } |
| } |
|
|
| destroy() { |
| if (this.unsubscribe) { |
| this.unsubscribe(); |
| } |
| } |
| } |