// TaskForge Pro - Main JavaScript // State management window.state = { tasks: [], tags: [], currentView: 'home', focusTask: null, pomodoro: { isRunning: false, isBreak: false, timeRemaining: 0, totalTime: 0, interval: null }, timelineTasks: [] }; // Initialize document.addEventListener('DOMContentLoaded', () => { loadFromLocalStorage(); initializeUI(); setupEventListeners(); render(); }); // Local Storage function loadFromLocalStorage() { const savedTasks = localStorage.getItem('tasks'); const savedTags = localStorage.getItem('tags'); const savedTimeline = localStorage.getItem('timeline'); if (savedTasks) { state.tasks = JSON.parse(savedTasks); } if (savedTags) { state.tags = JSON.parse(savedTags); } if (savedTimeline) { state.timelineTasks = JSON.parse(savedTimeline); } // Load Pomodoro settings const workTime = localStorage.getItem('pomodoroWorkTime') || 25; const breakTime = localStorage.getItem('pomodoroBreakTime') || 5; document.getElementById('pomodoro-work').value = workTime; document.getElementById('pomodoro-break').value = breakTime; } function saveToLocalStorage() { localStorage.setItem('tasks', JSON.stringify(state.tasks)); localStorage.setItem('tags', JSON.stringify(state.tags)); localStorage.setItem('timeline', JSON.stringify(state.timelineTasks)); } // UI Initialization function initializeUI() { // Current time display updateCurrentTime(); setInterval(updateCurrentTime, 1000); // Initialize timeline initializeTimeline(); // Scroll to current time in timeline setTimeout(scrollToCurrentTime, 100); // Check for default tags if (state.tags.length === 0) { state.tags = [ { id: '1', name: 'Wichtig', color: '#ef4444' }, { id: '2', name: 'Privat', color: '#10b981' }, { id: '3', name: 'Arbeit', color: '#3b82f6' } ]; saveToLocalStorage(); } } function updateCurrentTime() { const now = new Date(); const timeString = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); document.getElementById('current-time').textContent = timeString; } function setupEventListeners() { // Navigation document.querySelectorAll('.nav-btn').forEach(btn => { btn.addEventListener('click', (e) => { const view = e.currentTarget.dataset.view; switchView(view); }); }); // FAB buttons document.getElementById('add-task-fab').addEventListener('click', openAddTaskModal); document.getElementById('focus-fab').addEventListener('click', enterFocusMode); // Add task modal document.getElementById('add-task-form').addEventListener('submit', handleAddTask); document.getElementById('cancel-task').addEventListener('click', closeAddTaskModal); // Settings document.getElementById('import-btn').addEventListener('click', handleImport); document.getElementById('export-btn').addEventListener('click', handleExport); document.getElementById('add-tag-btn').addEventListener('click', handleAddTag); document.getElementById('pomodoro-work').addEventListener('change', savePomodoroSettings); document.getElementById('pomodoro-break').addEventListener('change', savePomodoroSettings); // Focus mode document.getElementById('focus-pause').addEventListener('click', pausePomodoro); document.getElementById('focus-play').addEventListener('click', resumePomodoro); document.getElementById('focus-stop').addEventListener('click', stopPomodoro); // Timeline document.getElementById('timeline-now').addEventListener('click', scrollToCurrentTime); } // View Management function switchView(view) { state.currentView = view; // Update nav buttons document.querySelectorAll('.nav-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.view === view); }); // Hide all views first document.querySelectorAll('.view').forEach(v => { v.classList.remove('active'); v.style.display = 'none'; }); // Show only the active view const activeView = document.getElementById(`${view}-view`); if (activeView) { activeView.style.display = 'block'; activeView.classList.add('active'); } if (view === 'timeline') { renderTimeline(); } } // Task Management function addTask(task) { const newTask = { id: Date.now().toString(), title: task.title, completed: false, estimatedTime: task.estimatedTime, // in minutes spentTime: 0, // in minutes tags: task.tags || [], createdAt: new Date().toISOString() }; state.tasks.push(newTask); saveToLocalStorage(); render(); return newTask; } function updateTask(taskId, updates) { const task = state.tasks.find(t => t.id === taskId); if (task) { Object.assign(task, updates); saveToLocalStorage(); render(); } } function deleteTask(taskId) { state.tasks = state.tasks.filter(t => t.id !== taskId); state.timelineTasks = state.timelineTasks.filter(t => t.taskId !== taskId); saveToLocalStorage(); render(); } function toggleTaskComplete(taskId) { const task = state.tasks.find(t => t.id === taskId); if (task) { task.completed = !task.completed; saveToLocalStorage(); // Animate task completion const taskElement = document.querySelector(`task-item[task-id="${taskId}"]`); if (taskElement) { taskElement.classList.add('completed'); setTimeout(() => { render(); }, 600); // Match animation duration } else { render(); } } } // Modal Functions function openAddTaskModal() { const modal = document.getElementById('add-task-modal'); modal.classList.remove('hidden'); renderModalTags(); document.getElementById('task-title').focus(); } function closeAddTaskModal() { const modal = document.getElementById('add-task-modal'); modal.classList.add('hidden'); document.getElementById('add-task-form').reset(); } function handleAddTask(e) { e.preventDefault(); const title = document.getElementById('task-title').value; const hours = parseInt(document.getElementById('task-hours').value) || 0; const minutes = parseInt(document.getElementById('task-minutes').value) || 0; const estimatedTime = hours * 60 + minutes; const selectedTags = Array.from(document.querySelectorAll('#modal-tags-list input:checked')).map(cb => cb.value); addTask({ title, estimatedTime, tags: selectedTags }); closeAddTaskModal(); } function renderModalTags() { const container = document.getElementById('modal-tags-list'); container.innerHTML = state.tags.map(tag => ` `).join(''); } // Tag Management function addTag(tag) { state.tags.push({ id: Date.now().toString(), name: tag.name, color: tag.color }); saveToLocalStorage(); render(); } function deleteTag(tagId) { state.tags = state.tags.filter(t => t.id !== tagId); // Remove tag from all tasks state.tasks.forEach(task => { task.tags = task.tags.filter(t => t !== tagId); }); saveToLocalStorage(); render(); } function handleAddTag() { const name = document.getElementById('new-tag-name').value; const color = document.getElementById('new-tag-color').value; if (name) { addTag({ name, color }); document.getElementById('new-tag-name').value = ''; } } // Import/Export function handleImport() { const markdown = document.getElementById('import-md').value; if (!markdown) return; const tasks = parseMarkdown(markdown); tasks.forEach(task => { // Check if task already exists const exists = state.tasks.some(t => t.title === task.title); if (!exists) { state.tasks.push({ id: Date.now().toString() + Math.random(), title: task.title, completed: task.completed, estimatedTime: task.estimatedTime, spentTime: task.spentTime || 0, tags: [], createdAt: new Date().toISOString() }); } }); saveToLocalStorage(); render(); document.getElementById('import-md').value = ''; } function handleExport() { const markdown = generateMarkdown(); const blob = new Blob([markdown], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'tasks.md'; a.click(); URL.revokeObjectURL(url); } function parseMarkdown(markdown) { const lines = markdown.split('\n'); const tasks = []; const taskRegex = /^\s*-\s*\[([ x])\]\s+(.+?)(?:\s*\|\s*(.+?)(?:\s*\|\s*(.+?))?)?\s*$/; lines.forEach(line => { const match = line.match(taskRegex); if (match) { const completed = match[1] === 'x'; const title = match[2].trim(); const timePart = match[3] ? match[3].trim() : '0h'; const tagsPart = match[4] ? match[4].trim() : ''; // Parse time const timeMatch = timePart.match(/(\d+)(?:\s*h\s*)?(?:(?:\s*(\d+)\s*m\s*)?)?/); const hours = parseInt(timeMatch[1]) || 0; const minutes = parseInt(timeMatch[2]) || 0; const estimatedTime = hours * 60 + minutes; tasks.push({ title, completed, estimatedTime }); } }); return tasks; } function generateMarkdown() { return state.tasks.map(task => { const status = task.completed ? 'x' : ' '; const hours = Math.floor(task.estimatedTime / 60); const minutes = task.estimatedTime % 60; const timeStr = `${hours}h${minutes > 0 ? ` ${minutes}m` : ''}`; return `- [${status}] ${task.title} | ${timeStr}`; }).join('\n'); } // Rendering function render() { renderTasks(); renderProgressBars(); renderTags(); renderTimeline(); } function renderTasks() { const openContainer = document.getElementById('open-tasks'); const completedContainer = document.getElementById('completed-tasks'); const openTasks = state.tasks.filter(t => !t.completed); const completedTasks = state.tasks.filter(t => t.completed); // Render open tasks if (openTasks.length === 0) { openContainer.innerHTML = '

Keine offenen Aufgaben

'; } else { openContainer.innerHTML = ''; openTasks.forEach(task => { const taskElement = document.createElement('task-item'); taskElement.setAttribute('task-id', task.id); taskElement.setAttribute('completed', 'false'); openContainer.appendChild(taskElement); }); } // Render completed tasks if (completedTasks.length === 0) { completedContainer.innerHTML = '

Keine erledigten Aufgaben

'; } else { completedContainer.innerHTML = ''; completedTasks.forEach(task => { const taskElement = document.createElement('task-item'); taskElement.setAttribute('task-id', task.id); taskElement.setAttribute('completed', 'true'); completedContainer.appendChild(taskElement); }); } document.getElementById('open-count').textContent = openTasks.length; document.getElementById('completed-count').textContent = completedTasks.length; // Replace feather icons feather.replace(); } function renderProgressBars() { // Overall progress const totalEstimated = state.tasks.reduce((sum, t) => sum + t.estimatedTime, 0) || 1; const totalSpent = state.tasks.reduce((sum, t) => sum + t.spentTime, 0); const totalProgress = (totalSpent / totalEstimated) * 100; document.getElementById('total-progress-bar').style.width = `${totalProgress}%`; document.getElementById('total-progress-text').textContent = `${Math.round(totalProgress)}% (${formatTime(totalSpent)} / ${formatTime(totalEstimated)})`; // Tag progress bars - only for tags with tasks const container = document.getElementById('tag-progress-container'); container.innerHTML = state.tags .filter(tag => state.tasks.some(t => t.tags.includes(tag.id))) // Only tags with tasks .map(tag => { const tagTasks = state.tasks.filter(t => t.tags.includes(tag.id)); const tagEstimated = tagTasks.reduce((sum, t) => sum + t.estimatedTime, 0) || 1; const tagSpent = tagTasks.reduce((sum, t) => sum + t.spentTime, 0); const tagProgress = (tagSpent / tagEstimated) * 100; return `
${tag.name} ${Math.round(tagProgress)}% (${formatTime(tagSpent)} / ${formatTime(tagEstimated)})
`; }).join(''); } function renderTags() { const container = document.getElementById('tags-list'); container.innerHTML = state.tags.map(tag => ` `).join(''); // Render tag components document.querySelectorAll('tag-item').forEach(el => { const tagId = el.getAttribute('tag-id'); el.addEventListener('delete', () => deleteTag(tagId)); }); } function renderTimeline() { if (state.currentView !== 'timeline') return; const container = document.getElementById('timeline-hours'); container.innerHTML = ''; // Generate hours for (let h = 6; h <= 22; h++) { const hourDiv = document.createElement('div'); hourDiv.className = 'timeline-hour relative border-b border-gray-700'; hourDiv.dataset.hour = h; hourDiv.style.minHeight = '60px'; const timeLabel = document.createElement('span'); timeLabel.className = 'absolute -ml-20 mt-2 text-sm text-gray-400 w-16 text-right'; timeLabel.textContent = `${h.toString().padStart(2, '0')}:00`; hourDiv.appendChild(timeLabel); container.appendChild(hourDiv); } // Highlight current hour highlightCurrentHour(); // Render scheduled tasks renderScheduledTasks(); } function highlightCurrentHour() { const now = new Date(); const currentHour = now.getHours(); const currentMinute = now.getMinutes(); // Highlight current hour document.querySelectorAll('.timeline-hour').forEach(el => { el.classList.remove('bg-gray-750'); }); const hourElement = document.querySelector(`.timeline-hour[data-hour="${currentHour}"]`); if (hourElement) { hourElement.classList.add('bg-gray-750'); } // Add current time indicator line document.getElementById('current-time-indicator')?.remove(); const timelineContainer = document.getElementById('timeline-hours'); if (timelineContainer) { const indicator = document.createElement('div'); indicator.id = 'current-time-indicator'; indicator.style.position = 'absolute'; indicator.style.left = '0'; indicator.style.right = '0'; indicator.style.height = '2px'; indicator.style.backgroundColor = '#ef4444'; indicator.style.zIndex = '10'; const hourElement = document.querySelector(`.timeline-hour[data-hour="${currentHour}"]`); if (hourElement) { const hourTop = hourElement.offsetTop; const hourHeight = hourElement.offsetHeight; const minutePosition = (currentMinute / 60) * hourHeight; indicator.style.top = `${hourTop + minutePosition}px`; timelineContainer.appendChild(indicator); // Add time label const timeLabel = document.createElement('div'); timeLabel.textContent = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); timeLabel.style.position = 'absolute'; timeLabel.style.left = '-64px'; timeLabel.style.top = `${hourTop + minutePosition - 10}px`; timeLabel.style.backgroundColor = '#ef4444'; timeLabel.style.color = 'white'; timeLabel.style.padding = '2px 8px'; timeLabel.style.borderRadius = '4px'; timeLabel.style.fontSize = '12px'; timeLabel.style.zIndex = '10'; hourEl.appendChild(timeLabel); } } } function renderScheduledTasks() { const today = new Date().toDateString(); const scheduledTasks = state.timelineTasks.filter(t => new Date(t.date).toDateString() === today ); scheduledTasks.forEach(task => { const taskData = state.tasks.find(t => t.id === task.taskId); if (!taskData) return; const startHour = new Date(task.startTime).getHours(); const startMinute = new Date(task.startTime).getMinutes(); const duration = taskData.estimatedTime; const hourEl = document.querySelector(`[data-hour="${startHour}"]`); if (hourEl) { const taskEl = document.createElement('div'); taskEl.className = 'timeline-task-block'; taskEl.style.left = `${(startMinute / 60) * 100}%`; taskEl.style.width = `${(duration / 60) * 100}%`; taskEl.style.backgroundColor = task.tags[0] ? state.tags.find(t => t.id === task.tags[0])?.color + '20' : '#f9731620'; taskEl.textContent = taskData.title; taskEl.draggable = true; hourEl.appendChild(taskEl); } }); } function scrollToCurrentTime() { const now = new Date(); const currentHour = now.getHours(); const hourEl = document.querySelector(`[data-hour="${currentHour}"]`); if (hourEl) { hourEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } // Focus Mode & Pomodoro function enterFocusMode() { // Open a task selection modal const openTasks = state.tasks.filter(t => !t.completed); if (openTasks.length === 0) { alert('Keine offenen Aufgaben für den Fokus-Modus!'); return; } const modal = document.createElement('div'); modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'; modal.innerHTML = `

Aufgabe für Fokus-Modus auswählen

${openTasks.map(task => `
${task.title}
${formatTime(task.estimatedTime)}
`).join('')}
`; document.body.appendChild(modal); } function startFocusModeWithId(taskId) { const task = state.tasks.find(t => t.id === taskId); if (task) { document.querySelector('div[class*="fixed"]')?.remove(); startFocusMode(task); } } function startFocusMode(task) { state.focusTask = task; document.getElementById('focus-task-title').textContent = task.title; document.getElementById('focus-mode').classList.remove('hidden'); // Start pomodoro const workTime = parseInt(localStorage.getItem('pomodoroWorkTime')) || 25; startPomodoro(workTime * 60, false); } function startPomodoro(seconds, isBreak) { state.pomodoro.isRunning = true; state.pomodoro.isBreak = isBreak; state.pomodoro.timeRemaining = seconds; state.pomodoro.totalTime = seconds; clearInterval(state.pomodoro.interval); state.pomodoro.interval = setInterval(() => { if (state.pomodoro.timeRemaining > 0) { state.pomodoro.timeRemaining--; updatePomodoroDisplay(); } else { handlePomodoroComplete(); } }, 1000); updatePomodoroDisplay(); } function updatePomodoroDisplay() { const minutes = Math.floor(state.pomodoro.timeRemaining / 60); const seconds = state.pomodoro.timeRemaining % 60; document.getElementById('pomodoro-timer').textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; const statusEl = document.getElementById('pomodoro-status'); statusEl.textContent = state.pomodoro.isBreak ? 'Pausenzeit läuft...' : 'Arbeitszeit läuft...'; } function handlePomodoroComplete() { clearInterval(state.pomodoro.interval); if (!state.pomodoro.isBreak) { // Work session complete const elapsed = state.pomodoro.totalTime; state.focusTask.spentTime += elapsed; saveToLocalStorage(); // Start break const breakTime = parseInt(localStorage.getItem('pomodoroBreakTime')) || 5; startPomodoro(breakTime * 60, true); // Notification if ('Notification' in window && Notification.permission === 'granted') { new Notification('Pause!', { body: 'Arbeitszeit abgeschlossen. Zeit für eine Pause!' }); } } else { // Break complete if ('Notification' in window && Notification.permission === 'granted') { new Notification('Weiterarbeiten!', { body: 'Pausenzeit ist vorbei.' }); } } } function pausePomodoro() { state.pomodoro.isRunning = false; clearInterval(state.pomodoro.interval); document.getElementById('focus-pause').classList.add('hidden'); document.getElementById('focus-play').classList.remove('hidden'); } function resumePomodoro() { startPomodoro(state.pomodoro.timeRemaining, state.pomodoro.isBreak); document.getElementById('focus-play').classList.add('hidden'); document.getElementById('focus-pause').classList.remove('hidden'); } function stopPomodoro() { clearInterval(state.pomodoro.interval); // Add elapsed time to task if (state.focusTask && !state.pomodoro.isBreak) { const elapsed = state.pomodoro.totalTime - state.pomodoro.timeRemaining; state.focusTask.spentTime += elapsed; saveToLocalStorage(); } state.focusTask = null; state.pomodoro.isRunning = false; document.getElementById('focus-mode').classList.add('hidden'); render(); } function savePomodoroSettings() { localStorage.setItem('pomodoroWorkTime', document.getElementById('pomodoro-work').value); localStorage.setItem('pomodoroBreakTime', document.getElementById('pomodoro-break').value); } // Timeline Functions function initializeTimeline() { // Initialize timeline sidebar const timelineSidebar = document.createElement('timeline-sidebar'); document.body.appendChild(timelineSidebar); // Listen for task drops timelineSidebar.addEventListener('task-dropped', (e) => { const { taskId, hour } = e.detail; scheduleTask(taskId, hour, 0); // Schedule at top of hour }); // Setup drag start for task items document.addEventListener('dragstart', (e) => { if (e.target.tagName === 'TASK-ITEM') { const taskId = e.target.getAttribute('task-id'); e.dataTransfer.setData('text/plain', taskId); } }); } function handleDragStart(e) { if (e.target.classList.contains('task-item')) { const taskId = e.target.getAttribute('task-id'); e.dataTransfer.setData('text/plain', taskId); e.target.classList.add('dragging'); } } function handleDragOver(e) { e.preventDefault(); if (e.target.classList.contains('timeline-hour')) { e.target.classList.add('drag-over'); } } function handleDrop(e) { e.preventDefault(); // Remove drag-over class from all elements document.querySelectorAll('.drag-over').forEach(el => { el.classList.remove('drag-over'); }); const taskId = e.dataTransfer.getData('text/plain'); const hourEl = e.target.closest('.timeline-hour'); if (hourEl && taskId) { const hour = parseInt(hourEl.dataset.hour); const rect = hourEl.getBoundingClientRect(); const x = e.clientX - rect.left; const hourWidth = rect.width; const minute = Math.floor((x / hourWidth) * 60); scheduleTask(taskId, hour, minute); } } function handleDragEnd(e) { e.target.classList.remove('dragging'); document.querySelectorAll('.drag-over').forEach(el => { el.classList.remove('drag-over'); }); } function scheduleTask(taskId, hour, minute) { const taskDate = new Date(); taskDate.setHours(0, 0, 0, 0); // Set to start of day for consistent date matching const startTime = new Date(); startTime.setHours(hour, minute, 0, 0); // Remove existing schedule for this task today state.timelineTasks = state.timelineTasks.filter(t => { const scheduleDate = new Date(t.date); scheduleDate.setHours(0, 0, 0, 0); return !(scheduleDate.getTime() === taskDate.getTime() && t.taskId === taskId); }); // Add new schedule state.timelineTasks.push({ taskId, date: taskDate.toISOString(), startTime: startTime.toISOString() }); saveToLocalStorage(); renderTimeline(); } // Utilities function formatTime(minutes) { const h = Math.floor(minutes / 60); const m = minutes % 60; return `${h}h${m > 0 ? ` ${m}m` : ''}`; } // Request notification permission if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission(); }