taskforge-pro / script.js
Fadikkop's picture
Promote version 4549c0d to main
a12aab2 verified
// 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();
}