Hydrant129's picture
Initial DeepSite commit
13ab0ed verified
Raw
History Blame Contribute Delete
14.6 kB
/**
* Main Layout Component
* Sidebar + Header + Content Area
*/
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>
`;
// Initialize icons
lucide.createIcons();
// Bind events
this.bindEvents();
// Render content
const mainContent = this.container.querySelector('#mainContent');
if (contentComponent && mainContent) {
contentComponent.render(mainContent);
}
// Subscribe to state changes
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() {
// Sidebar toggle
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);
});
// Dark mode
this.container.querySelector('#toggleDarkMode')?.addEventListener('click', () => {
store.toggleDarkMode();
});
// Search
const searchInput = this.container.querySelector('#globalSearch');
searchInput?.addEventListener('input', (e) => {
store.setUISState('searchQuery', e.target.value);
});
// Add Project
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() {
// Update project list
const projectList = this.container.querySelector('#projectList');
if (projectList) {
projectList.innerHTML = this.renderProjectList();
}
// Update search value if changed elsewhere
const searchInput = this.container.querySelector('#globalSearch');
if (searchInput && searchInput.value !== store.state.ui.searchQuery) {
searchInput.value = store.state.ui.searchQuery;
}
// Update icons
lucide.createIcons();
}
showAddProjectModal() {
// Einfache Prompt-Implementation für Demo
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();
}
}
}