Spaces:
Running
Running
| // 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 => ` | |
| <label class="flex items-center space-x-2 cursor-pointer"> | |
| <input type="checkbox" value="${tag.id}" class="custom-checkbox"> | |
| <span class="tag-badge" style="background-color: ${tag.color}20; color: ${tag.color}"> | |
| ${tag.name} | |
| </span> | |
| </label> | |
| `).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 = '<div class="empty-state"><i data-feather="inbox"></i><p>Keine offenen Aufgaben</p></div>'; | |
| } 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 = '<div class="empty-state"><i data-feather="check-circle"></i><p>Keine erledigten Aufgaben</p></div>'; | |
| } 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 ` | |
| <div class="bg-gray-800 rounded-lg p-4 shadow transition-all hover:scale-[1.02]"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <span class="font-medium" style="color: ${tag.color}">${tag.name}</span> | |
| <span class="text-sm text-gray-400"> | |
| ${Math.round(tagProgress)}% (${formatTime(tagSpent)} / ${formatTime(tagEstimated)}) | |
| </span> | |
| </div> | |
| <div class="w-full bg-gray-700 rounded-full h-2 overflow-hidden"> | |
| <div class="h-full rounded-full transition-all duration-500" | |
| style="width: ${tagProgress}%; background-color: ${tag.color}"></div> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| function renderTags() { | |
| const container = document.getElementById('tags-list'); | |
| container.innerHTML = state.tags.map(tag => ` | |
| <tag-item tag-id="${tag.id}" tag-name="${tag.name}" tag-color="${tag.color}"></tag-item> | |
| `).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 = ` | |
| <div class="bg-gray-800 rounded-xl p-6 max-w-md w-full max-h-[80vh] overflow-y-auto"> | |
| <h3 class="text-xl font-semibold mb-4">Aufgabe für Fokus-Modus auswählen</h3> | |
| <div class="space-y-2"> | |
| ${openTasks.map(task => ` | |
| <div class="bg-gray-700 p-3 rounded-lg cursor-pointer hover:bg-gray-600 transition" | |
| onclick="startFocusModeWithId('${task.id}')"> | |
| <div class="font-medium">${task.title}</div> | |
| <div class="text-sm text-gray-400">${formatTime(task.estimatedTime)}</div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| <button onclick="this.closest('div[class*=\"fixed\"]').remove()" | |
| class="mt-4 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition w-full"> | |
| Abbrechen | |
| </button> | |
| </div> | |
| `; | |
| 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(); | |
| } |