Spaces:
Running
Running
Promote version 4549c0d to main
Browse filesPromoted commit 4549c0d2bdafddf68bbb5a3ac5890cefe286b3ca to main branch
- components/tag-item.js +109 -0
- components/task-item.js +303 -49
- components/timeline-sidebar.js +133 -252
- components/timeline-tasks.js +50 -179
- index.html +210 -797
- script.js +738 -179
- style.css +186 -769
components/tag-item.js
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
|
|
|
|
|
|
|
|
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Tag Item Component
|
| 2 |
+
class TagItem extends HTMLElement {
|
| 3 |
+
constructor() {
|
| 4 |
+
super();
|
| 5 |
+
this.attachShadow({ mode: 'open' });
|
| 6 |
+
}
|
| 7 |
|
| 8 |
+
connectedCallback() {
|
| 9 |
+
this.render();
|
| 10 |
+
this.setupEventListeners();
|
| 11 |
+
}
|
| 12 |
|
| 13 |
+
static get observedAttributes() {
|
| 14 |
+
return ['tag-id', 'tag-name', 'tag-color'];
|
| 15 |
+
}
|
| 16 |
|
| 17 |
+
attributeChangedCallback() {
|
| 18 |
+
this.render();
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
render() {
|
| 22 |
+
const tagName = this.getAttribute('tag-name') || '';
|
| 23 |
+
const tagColor = this.getAttribute('tag-color') || '#f97316';
|
| 24 |
+
const tagId = this.getAttribute('tag-id') || '';
|
| 25 |
+
|
| 26 |
+
this.shadowRoot.innerHTML = `
|
| 27 |
+
<style>
|
| 28 |
+
:host {
|
| 29 |
+
display: inline-flex;
|
| 30 |
+
align-items: center;
|
| 31 |
+
gap: 6px;
|
| 32 |
+
padding: 4px 12px;
|
| 33 |
+
border-radius: 16px;
|
| 34 |
+
font-size: 14px;
|
| 35 |
+
font-weight: 500;
|
| 36 |
+
transition: all 0.2s ease;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.tag-content {
|
| 40 |
+
background-color: ${tagColor}20;
|
| 41 |
+
color: ${tagColor};
|
| 42 |
+
padding: 4px 12px;
|
| 43 |
+
border-radius: 16px;
|
| 44 |
+
display: flex;
|
| 45 |
+
align-items: center;
|
| 46 |
+
gap: 6px;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.tag-name {
|
| 50 |
+
user-select: none;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.delete-btn {
|
| 54 |
+
background: none;
|
| 55 |
+
border: none;
|
| 56 |
+
color: inherit;
|
| 57 |
+
cursor: pointer;
|
| 58 |
+
padding: 2px;
|
| 59 |
+
border-radius: 50%;
|
| 60 |
+
display: flex;
|
| 61 |
+
align-items: center;
|
| 62 |
+
justify-content: center;
|
| 63 |
+
transition: all 0.2s ease;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.delete-btn:hover {
|
| 67 |
+
background-color: rgba(0, 0, 0, 0.1);
|
| 68 |
+
transform: scale(1.1);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.delete-btn i {
|
| 72 |
+
width: 14px;
|
| 73 |
+
height: 14px;
|
| 74 |
+
}
|
| 75 |
+
</style>
|
| 76 |
+
|
| 77 |
+
<div class="tag-content">
|
| 78 |
+
<span class="tag-name">${tagName}</span>
|
| 79 |
+
<button class="delete-btn" @click="${this.handleDelete}">
|
| 80 |
+
<i data-feather="x"></i>
|
| 81 |
+
</button>
|
| 82 |
+
</div>
|
| 83 |
+
`;
|
| 84 |
+
|
| 85 |
+
// Replace feather icon
|
| 86 |
+
const iconEl = this.shadowRoot.querySelector('i[data-feather]');
|
| 87 |
+
if (iconEl) {
|
| 88 |
+
iconEl.outerHTML = feather.icons.x.toSvg({ width: 14, height: 14 });
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
setupEventListeners() {
|
| 93 |
+
this.shadowRoot.addEventListener('click', (e) => {
|
| 94 |
+
const deleteBtn = e.target.closest('.delete-btn');
|
| 95 |
+
if (deleteBtn) {
|
| 96 |
+
e.stopPropagation();
|
| 97 |
+
this.handleDelete(e);
|
| 98 |
+
}
|
| 99 |
+
});
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
handleDelete(e) {
|
| 103 |
+
if (confirm('Tag wirklich löschen?')) {
|
| 104 |
+
this.dispatchEvent(new CustomEvent('delete', {
|
| 105 |
+
bubbles: true,
|
| 106 |
+
detail: { tagId: this.getAttribute('tag-id') }
|
| 107 |
+
}));
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
customElements.define('tag-item', TagItem);
|
components/task-item.js
CHANGED
|
@@ -1,101 +1,355 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
class TagItem extends HTMLElement {
|
| 4 |
constructor() {
|
| 5 |
super();
|
| 6 |
this.attachShadow({ mode: 'open' });
|
| 7 |
}
|
| 8 |
-
|
| 9 |
connectedCallback() {
|
|
|
|
|
|
|
|
|
|
| 10 |
this.render();
|
| 11 |
this.setupEventListeners();
|
| 12 |
}
|
| 13 |
|
| 14 |
static get observedAttributes() {
|
| 15 |
-
return ['
|
| 16 |
}
|
| 17 |
|
| 18 |
-
attributeChangedCallback() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
this.render();
|
| 20 |
}
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
<style>
|
| 29 |
:host {
|
| 30 |
-
display:
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
padding:
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
transition: all 0.2s ease;
|
| 38 |
}
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
display: flex;
|
| 46 |
align-items: center;
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
-
.
|
| 51 |
-
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
-
.
|
| 55 |
background: none;
|
| 56 |
border: none;
|
| 57 |
-
color:
|
| 58 |
cursor: pointer;
|
| 59 |
-
padding:
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
display: flex;
|
| 62 |
align-items: center;
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
transition: all 0.2s ease;
|
| 65 |
}
|
| 66 |
|
| 67 |
-
.
|
| 68 |
-
background
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
<div class="
|
| 74 |
-
<span class="
|
| 75 |
-
<
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
</div>
|
| 79 |
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
}
|
| 81 |
-
|
|
|
|
| 82 |
this.shadowRoot.addEventListener('click', (e) => {
|
| 83 |
-
const
|
| 84 |
-
if (
|
| 85 |
e.stopPropagation();
|
| 86 |
-
this.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
});
|
| 89 |
}
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
handleDelete(e) {
|
| 92 |
-
if (confirm('
|
| 93 |
-
this.dispatchEvent(new CustomEvent('delete', {
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
}));
|
| 97 |
}
|
| 98 |
}
|
| 99 |
}
|
| 100 |
|
| 101 |
-
customElements.define('
|
|
|
|
| 1 |
+
// Task Item Component
|
| 2 |
+
class TaskItem extends HTMLElement {
|
|
|
|
| 3 |
constructor() {
|
| 4 |
super();
|
| 5 |
this.attachShadow({ mode: 'open' });
|
| 6 |
}
|
|
|
|
| 7 |
connectedCallback() {
|
| 8 |
+
this.taskId = this.getAttribute('task-id');
|
| 9 |
+
this.completed = this.getAttribute('completed') === 'true';
|
| 10 |
+
this.loadTaskData();
|
| 11 |
this.render();
|
| 12 |
this.setupEventListeners();
|
| 13 |
}
|
| 14 |
|
| 15 |
static get observedAttributes() {
|
| 16 |
+
return ['task-id', 'completed'];
|
| 17 |
}
|
| 18 |
|
| 19 |
+
attributeChangedCallback(name, oldValue, newValue) {
|
| 20 |
+
if (name === 'task-id' && oldValue !== newValue) {
|
| 21 |
+
this.taskId = newValue;
|
| 22 |
+
this.loadTaskData();
|
| 23 |
+
} else if (name === 'completed') {
|
| 24 |
+
this.completed = newValue === 'true';
|
| 25 |
+
}
|
| 26 |
this.render();
|
| 27 |
}
|
| 28 |
|
| 29 |
+
loadTaskData() {
|
| 30 |
+
if (!window.state || !window.state.tasks) return;
|
| 31 |
+
|
| 32 |
+
this.task = window.state.tasks.find(t => t.id === this.taskId);
|
| 33 |
+
this.tags = window.state.tags || [];
|
| 34 |
+
}
|
| 35 |
|
| 36 |
+
render() {
|
| 37 |
+
if (!this.task) {
|
| 38 |
+
this.shadowRoot.innerHTML = `<div class="loading" data-task-id="${this.taskId}">Lädt...</div>`;
|
| 39 |
+
return;
|
| 40 |
+
}
|
| 41 |
+
const { title, estimatedTime, spentTime, tags } = this.task;
|
| 42 |
+
const progress = estimatedTime > 0 ? (spentTime / estimatedTime) * 100 : 0;
|
| 43 |
+
const tagElements = (tags || []).map(tagId => {
|
| 44 |
+
const tag = this.tags?.find(t => t.id === tagId);
|
| 45 |
+
return tag ? `<span class="tag-badge" style="background-color: ${tag.color}20; color: ${tag.color}">${tag.name}</span>` : '';
|
| 46 |
+
}).join('');
|
| 47 |
+
this.shadowRoot.innerHTML = `
|
| 48 |
<style>
|
| 49 |
:host {
|
| 50 |
+
display: block;
|
| 51 |
+
background: #1f2937;
|
| 52 |
+
border-radius: 8px;
|
| 53 |
+
padding: 12px;
|
| 54 |
+
margin-bottom: 8px;
|
| 55 |
+
transition: all 0.3s ease;
|
| 56 |
+
cursor: pointer;
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
+
:host(:hover) {
|
| 60 |
+
transform: translateY(-2px);
|
| 61 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
:host(.completed) {
|
| 65 |
+
opacity: 0.6;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.task-header {
|
| 69 |
display: flex;
|
| 70 |
align-items: center;
|
| 71 |
+
justify-content: space-between;
|
| 72 |
+
margin-bottom: 8px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.task-title {
|
| 76 |
+
font-size: 16px;
|
| 77 |
+
font-weight: 500;
|
| 78 |
+
color: #f3f4f6;
|
| 79 |
+
flex: 1;
|
| 80 |
+
margin-left: 8px;
|
| 81 |
+
text-decoration: none;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
:host(.completed) .task-title {
|
| 85 |
+
text-decoration: line-through;
|
| 86 |
+
color: #9ca3af;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.task-checkbox {
|
| 90 |
+
width: 20px;
|
| 91 |
+
height: 20px;
|
| 92 |
+
cursor: pointer;
|
| 93 |
}
|
| 94 |
|
| 95 |
+
.task-actions {
|
| 96 |
+
display: flex;
|
| 97 |
+
gap: 8px;
|
| 98 |
}
|
| 99 |
|
| 100 |
+
.task-action {
|
| 101 |
background: none;
|
| 102 |
border: none;
|
| 103 |
+
color: #6b7280;
|
| 104 |
cursor: pointer;
|
| 105 |
+
padding: 4px;
|
| 106 |
+
transition: color 0.2s ease;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.task-action:hover {
|
| 110 |
+
color: #f97316;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.task-info {
|
| 114 |
display: flex;
|
| 115 |
align-items: center;
|
| 116 |
+
gap: 16px;
|
| 117 |
+
margin-top: 8px;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.time-info {
|
| 121 |
+
font-size: 14px;
|
| 122 |
+
color: #9ca3af;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.progress-bar {
|
| 126 |
+
width: 100%;
|
| 127 |
+
height: 4px;
|
| 128 |
+
background: #374151;
|
| 129 |
+
border-radius: 2px;
|
| 130 |
+
overflow: hidden;
|
| 131 |
+
margin-top: 8px;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.progress-fill {
|
| 135 |
+
height: 100%;
|
| 136 |
+
background: linear-gradient(90deg, #f97316, #ea580c);
|
| 137 |
+
transition: width 0.3s ease;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.tags {
|
| 141 |
+
display: flex;
|
| 142 |
+
gap: 4px;
|
| 143 |
+
flex-wrap: wrap;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.tag-badge {
|
| 147 |
+
font-size: 12px;
|
| 148 |
+
padding: 2px 8px;
|
| 149 |
+
border-radius: 12px;
|
| 150 |
+
font-weight: 500;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.edit-form {
|
| 154 |
+
display: none;
|
| 155 |
+
margin-top: 12px;
|
| 156 |
+
padding-top: 12px;
|
| 157 |
+
border-top: 1px solid #374151;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.edit-form.active {
|
| 161 |
+
display: block;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.input-group {
|
| 165 |
+
display: flex;
|
| 166 |
+
gap: 8px;
|
| 167 |
+
margin-bottom: 8px;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.input-group input {
|
| 171 |
+
background: #374151;
|
| 172 |
+
border: 1px solid #4b5563;
|
| 173 |
+
border-radius: 4px;
|
| 174 |
+
padding: 6px 12px;
|
| 175 |
+
color: #f3f4f6;
|
| 176 |
+
font-size: 14px;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.input-group input:focus {
|
| 180 |
+
outline: none;
|
| 181 |
+
border-color: #f97316;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.edit-actions {
|
| 185 |
+
display: flex;
|
| 186 |
+
gap: 8px;
|
| 187 |
+
justify-content: flex-end;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.btn-small {
|
| 191 |
+
padding: 6px 12px;
|
| 192 |
+
border-radius: 4px;
|
| 193 |
+
border: none;
|
| 194 |
+
font-size: 14px;
|
| 195 |
+
cursor: pointer;
|
| 196 |
transition: all 0.2s ease;
|
| 197 |
}
|
| 198 |
|
| 199 |
+
.btn-save {
|
| 200 |
+
background: #059669;
|
| 201 |
+
color: white;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.btn-save:hover {
|
| 205 |
+
background: #047857;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.btn-cancel {
|
| 209 |
+
background: #374151;
|
| 210 |
+
color: #d1d5db;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.btn-cancel:hover {
|
| 214 |
+
background: #4b5563;
|
| 215 |
}
|
| 216 |
</style>
|
| 217 |
+
<div class="task-header">
|
| 218 |
+
<input type="checkbox" class="task-checkbox" ${this.completed ? 'checked' : ''}
|
| 219 |
+
@change="${this.handleToggleComplete}">
|
| 220 |
+
<span class="task-title">${title}</span>
|
| 221 |
+
<div class="task-actions">
|
| 222 |
+
<button class="focus-btn" @click="${this.handleFocus}" title="Pomodoro starten">
|
| 223 |
+
<i data-feather="clock"></i>
|
| 224 |
+
</button>
|
| 225 |
+
<button class="task-action" @click="${this.handleEdit}" title="Bearbeiten">
|
| 226 |
+
<i data-feather="edit-2"></i>
|
| 227 |
+
</button>
|
| 228 |
+
<button class="task-action" @click="${this.handleDelete}" title="Löschen">
|
| 229 |
+
<i data-feather="trash-2"></i>
|
| 230 |
+
</button>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
|
| 234 |
+
<div class="task-info">
|
| 235 |
+
<span class="time-info">${formatTime(spentTime)} / ${formatTime(estimatedTime)}</span>
|
| 236 |
+
<div class="tags">${tagElements}</div>
|
| 237 |
+
</div>
|
| 238 |
+
|
| 239 |
+
<div class="progress-bar">
|
| 240 |
+
<div class="progress-fill" style="width: ${progress}%"></div>
|
| 241 |
+
</div>
|
| 242 |
+
<div class="edit-form" id="edit-form-${this.taskId}">
|
| 243 |
+
<div class="input-group">
|
| 244 |
+
<input type="text" value="${title}" id="edit-title-${this.taskId}" placeholder="Titel">
|
| 245 |
+
<input type="number" value="${Math.floor(estimatedTime / 60)}" id="edit-hours-${this.taskId}"
|
| 246 |
+
placeholder="h" min="0" max="24" style="width: 60px">
|
| 247 |
+
<input type="number" value="${estimatedTime % 60}" id="edit-minutes-${this.taskId}"
|
| 248 |
+
placeholder="m" min="0" max="59" step="15" style="width: 60px">
|
| 249 |
+
<button class="btn-small btn-add-time" @click="${this.handleAddTime}" title="Zeit hinzufügen">
|
| 250 |
+
<i data-feather="clock" style="width: 14px; height: 14px;"></i>
|
| 251 |
+
</button>
|
| 252 |
+
</div>
|
| 253 |
+
<div class="edit-actions">
|
| 254 |
+
<button class="btn-small btn-save" @click="${this.handleSave}">Speichern</button>
|
| 255 |
+
<button class="btn-small btn-cancel" @click="${this.handleCancel}">Abbrechen</button>
|
| 256 |
+
</div>
|
| 257 |
</div>
|
| 258 |
`;
|
| 259 |
+
if (this.completed) {
|
| 260 |
+
this.classList.add('completed');
|
| 261 |
+
} else {
|
| 262 |
+
this.classList.remove('completed');
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// Replace feather icons
|
| 266 |
+
this.shadowRoot.querySelectorAll('i[data-feather]').forEach(icon => {
|
| 267 |
+
const iconName = icon.getAttribute('data-feather');
|
| 268 |
+
icon.outerHTML = feather.icons[iconName].toSvg({ width: 16, height: 16 });
|
| 269 |
+
});
|
| 270 |
}
|
| 271 |
+
|
| 272 |
+
setupEventListeners() {
|
| 273 |
this.shadowRoot.addEventListener('click', (e) => {
|
| 274 |
+
const action = e.target.closest('button')?.getAttribute('@click');
|
| 275 |
+
if (action) {
|
| 276 |
e.stopPropagation();
|
| 277 |
+
this[action.replace('${this.', '').replace('}', '')](e);
|
| 278 |
+
}
|
| 279 |
+
});
|
| 280 |
+
|
| 281 |
+
this.shadowRoot.addEventListener('change', (e) => {
|
| 282 |
+
if (e.target.classList.contains('task-checkbox')) {
|
| 283 |
+
e.stopPropagation();
|
| 284 |
+
this.handleToggleComplete(e);
|
| 285 |
}
|
| 286 |
});
|
| 287 |
}
|
| 288 |
|
| 289 |
+
handleToggleComplete(e) {
|
| 290 |
+
const completed = e.target.checked;
|
| 291 |
+
this.dispatchEvent(new CustomEvent('toggle-complete', {
|
| 292 |
+
detail: { taskId: this.getAttribute('task-id'), completed },
|
| 293 |
+
bubbles: true
|
| 294 |
+
}));
|
| 295 |
+
|
| 296 |
+
if (completed) {
|
| 297 |
+
this.classList.add('completed');
|
| 298 |
+
} else {
|
| 299 |
+
this.classList.remove('completed');
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
handleFocus(e) {
|
| 303 |
+
const taskId = this.getAttribute('task-id');
|
| 304 |
+
const task = state.tasks.find(t => t.id === taskId);
|
| 305 |
+
if (task) {
|
| 306 |
+
startFocusMode(task);
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
handleEdit(e) {
|
| 310 |
+
const editForm = this.shadowRoot.querySelector('.edit-form');
|
| 311 |
+
editForm.classList.add('active');
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
handleDelete(e) {
|
| 315 |
+
if (confirm('Aufgabe wirklich löschen?')) {
|
| 316 |
+
this.dispatchEvent(new CustomEvent('delete-task', {
|
| 317 |
+
detail: { taskId: this.getAttribute('task-id') },
|
| 318 |
+
bubbles: true
|
| 319 |
+
}));
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
handleSave(e) {
|
| 324 |
+
const taskId = this.getAttribute('task-id');
|
| 325 |
+
const title = this.shadowRoot.getElementById(`edit-title-${taskId}`).value;
|
| 326 |
+
const hours = parseInt(this.shadowRoot.getElementById(`edit-hours-${taskId}`).value) || 0;
|
| 327 |
+
const minutes = parseInt(this.shadowRoot.getElementById(`edit-minutes-${taskId}`).value) || 0;
|
| 328 |
+
const estimatedTime = hours * 60 + minutes;
|
| 329 |
+
|
| 330 |
+
this.dispatchEvent(new CustomEvent('update-task', {
|
| 331 |
+
detail: { taskId, updates: { title, estimatedTime } },
|
| 332 |
+
bubbles: true
|
| 333 |
+
}));
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
handleCancel(e) {
|
| 337 |
+
const editForm = this.shadowRoot.querySelector('.edit-form');
|
| 338 |
+
editForm.classList.remove('active');
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
handleAddTime(e) {
|
| 342 |
+
const minutes = prompt('Wie viele Minuten willst du hinzufügen?', '15');
|
| 343 |
+
if (minutes && !isNaN(minutes)) {
|
| 344 |
+
this.dispatchEvent(new CustomEvent('add-time', {
|
| 345 |
+
detail: {
|
| 346 |
+
taskId: this.getAttribute('task-id'),
|
| 347 |
+
minutes: parseInt(minutes)
|
| 348 |
+
},
|
| 349 |
+
bubbles: true
|
| 350 |
}));
|
| 351 |
}
|
| 352 |
}
|
| 353 |
}
|
| 354 |
|
| 355 |
+
customElements.define('task-item', TaskItem);
|
components/timeline-sidebar.js
CHANGED
|
@@ -1,273 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
this.loadTaskData();
|
| 13 |
-
this.render();
|
| 14 |
-
this.setupEventListeners();
|
| 15 |
-
}
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
this.taskId = newValue;
|
| 24 |
-
this.loadTaskData();
|
| 25 |
-
} else if (name === 'completed') {
|
| 26 |
-
this.completed = newValue === 'true';
|
| 27 |
}
|
| 28 |
-
this.render();
|
| 29 |
-
}
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
| 42 |
}
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
return tag ? `<span class="tag-badge" style="background-color: ${tag.color}20; color: ${tag.color}">${tag.name}</span>' : '';
|
| 48 |
-
}).join('');
|
| 49 |
-
|
| 50 |
-
this.shadowRoot.innerHTML = `
|
| 51 |
-
<style>
|
| 52 |
-
:host {
|
| 53 |
-
display: block;
|
| 54 |
-
background: #1f2937;
|
| 55 |
-
border-radius: 8px;
|
| 56 |
-
padding: 12px;
|
| 57 |
-
margin-bottom: 8px;
|
| 58 |
-
transition: all 0.3s ease;
|
| 59 |
-
cursor: pointer;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
:host(:hover) {
|
| 63 |
-
transform: translateY(-2px);
|
| 64 |
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
:host(.completed) {
|
| 68 |
-
opacity: 0.6;
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
.task-header {
|
| 72 |
-
display: flex;
|
| 73 |
-
align-items: center;
|
| 74 |
-
justify-content: space-between;
|
| 75 |
-
margin-bottom: 8px;
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
.task-title {
|
| 79 |
-
font-size: 16px;
|
| 80 |
-
font-weight: 500;
|
| 81 |
-
color: #f3f4f6;
|
| 82 |
-
flex: 1;
|
| 83 |
-
margin-left: 8px;
|
| 84 |
-
text-decoration: none;
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
:host(.completed) .task-title {
|
| 88 |
-
text-decoration: line-through;
|
| 89 |
-
color: #9ca3af;
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
.task-checkbox {
|
| 93 |
-
width: 20px;
|
| 94 |
-
height: 20px;
|
| 95 |
-
cursor: pointer;
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
.task-actions {
|
| 99 |
-
display: flex;
|
| 100 |
-
gap: 8px;
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
.task-action {
|
| 104 |
-
background: none;
|
| 105 |
-
border: none;
|
| 106 |
-
color: #6b7280;
|
| 107 |
-
cursor: pointer;
|
| 108 |
-
padding: 4px;
|
| 109 |
-
transition: color 0.2s ease;
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
.task-action:hover {
|
| 113 |
-
color: #f97316;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
.task-info {
|
| 117 |
-
display: flex;
|
| 118 |
-
align-items: center;
|
| 119 |
-
gap: 16px;
|
| 120 |
-
margin-top: 8px;
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
.time-info {
|
| 124 |
-
font-size: 14px;
|
| 125 |
-
color: #9ca3af;
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
.progress-bar {
|
| 129 |
-
width: 100%;
|
| 130 |
-
height: 4px;
|
| 131 |
-
background: #374151;
|
| 132 |
-
border-radius: 2px;
|
| 133 |
-
overflow: hidden;
|
| 134 |
-
margin-top: 8px;
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
.progress-fill {
|
| 138 |
-
height: 100%;
|
| 139 |
-
background: linear-gradient(90deg, #f97316, #ea580c);
|
| 140 |
-
transition: width 0.3s ease;
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
.tags {
|
| 144 |
-
display: flex;
|
| 145 |
-
gap: 4px;
|
| 146 |
-
flex-wrap: wrap;
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
.tag-badge {
|
| 150 |
-
font-size: 12px;
|
| 151 |
-
padding: 2px 8px;
|
| 152 |
-
border-radius: 12px;
|
| 153 |
-
font-weight: 500;
|
| 154 |
-
}
|
| 155 |
-
</style>
|
| 156 |
-
<div class="task-header">
|
| 157 |
-
<input type="checkbox" class="task-checkbox" ${this.completed ? 'checked' : ''}>
|
| 158 |
-
<span class="task-title">${title}</span>
|
| 159 |
-
<div class="task-actions">
|
| 160 |
-
<button class="task-action" title="Bearbeiten">
|
| 161 |
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 0-3-3L9 12"></path></svg>
|
| 162 |
-
</button>
|
| 163 |
-
<button class="task-action" title="Löschen">
|
| 164 |
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
| 165 |
-
</button>
|
| 166 |
-
</div>
|
| 167 |
-
</div>
|
| 168 |
-
|
| 169 |
-
<div class="task-info">
|
| 170 |
-
<span class="time-info">${formatTime(spentTime)} / ${formatTime(estimatedTime)}</span>
|
| 171 |
-
<div class="tags">${tagElements}</div>
|
| 172 |
-
</div>
|
| 173 |
-
|
| 174 |
-
<div class="progress-bar">
|
| 175 |
-
<div class="progress-fill" style="width: ${progress}%"></div>
|
| 176 |
-
</div>
|
| 177 |
-
`;
|
| 178 |
-
|
| 179 |
-
if (this.completed) {
|
| 180 |
-
this.classList.add('completed');
|
| 181 |
-
} else {
|
| 182 |
-
this.classList.remove('completed');
|
| 183 |
}
|
| 184 |
-
}
|
| 185 |
-
setupEventListeners() {
|
| 186 |
-
this.shadowRoot.addEventListener('click', (e) => {
|
| 187 |
-
const target = e.target.closest('button');
|
| 188 |
-
if (target) {
|
| 189 |
-
const action = target.getAttribute('title');
|
| 190 |
-
if (action === 'Bearbeiten') {
|
| 191 |
-
this.handleEdit(e);
|
| 192 |
-
} else if (action === 'Löschen') {
|
| 193 |
-
this.handleDelete(e);
|
| 194 |
-
} else if (action === 'Pomodoro starten') {
|
| 195 |
-
this.handleFocus(e);
|
| 196 |
-
}
|
| 197 |
-
}
|
| 198 |
-
});
|
| 199 |
-
|
| 200 |
-
this.shadowRoot.addEventListener('change', (e) => {
|
| 201 |
-
if (e.target.classList.contains('task-checkbox')) {
|
| 202 |
-
this.handleToggleComplete(e);
|
| 203 |
-
}
|
| 204 |
-
});
|
| 205 |
-
}
|
| 206 |
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
| 218 |
}
|
| 219 |
-
}
|
| 220 |
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
| 226 |
}
|
| 227 |
-
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
| 233 |
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
| 241 |
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
}));
|
| 253 |
-
}
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
}));
|
| 270 |
-
|
|
|
|
|
|
|
| 271 |
}
|
| 272 |
|
| 273 |
-
customElements.define('
|
|
|
|
| 1 |
+
class TimelineSidebar extends HTMLElement {
|
| 2 |
+
connectedCallback() {
|
| 3 |
+
this.attachShadow({ mode: 'open' });
|
| 4 |
+
this.render();
|
| 5 |
+
this.setupEventListeners();
|
| 6 |
+
}
|
| 7 |
|
| 8 |
+
render() {
|
| 9 |
+
this.shadowRoot.innerHTML = `
|
| 10 |
+
<style>
|
| 11 |
+
:host {
|
| 12 |
+
display: block;
|
| 13 |
+
position: fixed;
|
| 14 |
+
right: 0;
|
| 15 |
+
top: 0;
|
| 16 |
+
height: 100vh;
|
| 17 |
+
width: 300px;
|
| 18 |
+
background: #1f2937;
|
| 19 |
+
border-left: 1px solid #374151;
|
| 20 |
+
z-index: 30;
|
| 21 |
+
transform: translateX(100%);
|
| 22 |
+
transition: transform 0.3s ease;
|
| 23 |
+
overflow-y: auto;
|
| 24 |
+
}
|
| 25 |
|
| 26 |
+
:host(.open) {
|
| 27 |
+
transform: translateX(0);
|
| 28 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
+
.header {
|
| 31 |
+
padding: 1rem;
|
| 32 |
+
border-bottom: 1px solid #374151;
|
| 33 |
+
display: flex;
|
| 34 |
+
justify-content: space-between;
|
| 35 |
+
align-items: center;
|
| 36 |
+
}
|
| 37 |
|
| 38 |
+
.timeline-container {
|
| 39 |
+
padding: 1rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
.timeline-hour {
|
| 43 |
+
position: relative;
|
| 44 |
+
min-height: 60px;
|
| 45 |
+
border-bottom: 1px solid #374151;
|
| 46 |
+
padding-left: 50px;
|
| 47 |
+
}
|
| 48 |
|
| 49 |
+
.hour-label {
|
| 50 |
+
position: absolute;
|
| 51 |
+
left: 10px;
|
| 52 |
+
top: 5px;
|
| 53 |
+
font-size: 12px;
|
| 54 |
+
color: #9ca3af;
|
| 55 |
}
|
| 56 |
+
|
| 57 |
+
.task-drop-area {
|
| 58 |
+
height: 100%;
|
| 59 |
+
min-height: 60px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
+
.toggle-btn {
|
| 63 |
+
position: fixed;
|
| 64 |
+
right: 300px;
|
| 65 |
+
top: 50%;
|
| 66 |
+
transform: translateY(-50%);
|
| 67 |
+
background: #1f2937;
|
| 68 |
+
border: 1px solid #374151;
|
| 69 |
+
border-right: none;
|
| 70 |
+
padding: 1rem 0.5rem;
|
| 71 |
+
border-radius: 5px 0 0 5px;
|
| 72 |
+
cursor: pointer;
|
| 73 |
+
z-index: 30;
|
| 74 |
}
|
|
|
|
| 75 |
|
| 76 |
+
.task-block {
|
| 77 |
+
background: #374151;
|
| 78 |
+
border-left: 3px solid #f97316;
|
| 79 |
+
padding: 0.5rem;
|
| 80 |
+
margin: 0.5rem 0;
|
| 81 |
+
border-radius: 4px;
|
| 82 |
}
|
| 83 |
+
</style>
|
| 84 |
|
| 85 |
+
<button class="toggle-btn">
|
| 86 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 87 |
+
<polyline points="15 18 9 12 15 6"></polyline>
|
| 88 |
+
</svg>
|
| 89 |
+
</button>
|
| 90 |
|
| 91 |
+
<div class="header">
|
| 92 |
+
<h3>Timeline</h3>
|
| 93 |
+
<button id="close-timeline">
|
| 94 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 95 |
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
| 96 |
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
| 97 |
+
</svg>
|
| 98 |
+
</button>
|
| 99 |
+
</div>
|
| 100 |
|
| 101 |
+
<div class="timeline-container" id="timeline-hours">
|
| 102 |
+
${Array.from({ length: 24 }, (_, i) => `
|
| 103 |
+
<div class="timeline-hour" data-hour="${i}">
|
| 104 |
+
<span class="hour-label">${i.toString().padStart(2, '0')}:00</span>
|
| 105 |
+
<div class="task-drop-area" data-hour="${i}"></div>
|
| 106 |
+
</div>
|
| 107 |
+
`).join('')}
|
| 108 |
+
</div>
|
| 109 |
+
`;
|
| 110 |
+
}
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
setupEventListeners() {
|
| 113 |
+
const toggleBtn = this.shadowRoot.querySelector('.toggle-btn');
|
| 114 |
+
toggleBtn.addEventListener('click', () => {
|
| 115 |
+
this.classList.toggle('open');
|
| 116 |
+
const icon = toggleBtn.querySelector('svg');
|
| 117 |
+
if (this.classList.contains('open')) {
|
| 118 |
+
icon.innerHTML = '<polyline points="15 18 9 12 15 6"></polyline>';
|
| 119 |
+
} else {
|
| 120 |
+
icon.innerHTML = '<polyline points="9 18 15 12 9 6"></polyline>';
|
| 121 |
+
}
|
| 122 |
+
});
|
| 123 |
|
| 124 |
+
// Setup drag and drop
|
| 125 |
+
this.shadowRoot.addEventListener('dragover', (e) => {
|
| 126 |
+
if (e.target.classList.contains('task-drop-area')) {
|
| 127 |
+
e.preventDefault();
|
| 128 |
+
e.target.style.backgroundColor = '#37415170';
|
| 129 |
+
}
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
this.shadowRoot.addEventListener('dragleave', (e) => {
|
| 133 |
+
if (e.target.classList.contains('task-drop-area')) {
|
| 134 |
+
e.target.style.backgroundColor = '';
|
| 135 |
+
}
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
this.shadowRoot.addEventListener('drop', (e) => {
|
| 139 |
+
if (e.target.classList.contains('task-drop-area')) {
|
| 140 |
+
e.preventDefault();
|
| 141 |
+
e.target.style.backgroundColor = '';
|
| 142 |
+
|
| 143 |
+
const taskId = e.dataTransfer.getData('text/plain');
|
| 144 |
+
const hour = parseInt(e.target.dataset.hour);
|
| 145 |
+
|
| 146 |
+
this.dispatchEvent(new CustomEvent('task-dropped', {
|
| 147 |
+
detail: { taskId, hour }
|
| 148 |
}));
|
| 149 |
+
}
|
| 150 |
+
});
|
| 151 |
+
}
|
| 152 |
}
|
| 153 |
|
| 154 |
+
customElements.define('timeline-sidebar', TimelineSidebar);
|
components/timeline-tasks.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 1 |
-
class
|
| 2 |
connectedCallback() {
|
| 3 |
this.attachShadow({ mode: 'open' });
|
| 4 |
this.render();
|
| 5 |
-
this.setupEventListeners();
|
| 6 |
}
|
| 7 |
|
| 8 |
render() {
|
|
@@ -10,202 +9,74 @@ class TimelineSidebar extends HTMLElement {
|
|
| 10 |
<style>
|
| 11 |
:host {
|
| 12 |
display: block;
|
| 13 |
-
position: fixed;
|
| 14 |
-
right: 0;
|
| 15 |
-
top: 0;
|
| 16 |
-
height: 100vh;
|
| 17 |
-
width: 300px;
|
| 18 |
background: #1f2937;
|
| 19 |
border-left: 1px solid #374151;
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
overflow-y: auto;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
:host(.open) {
|
| 27 |
-
transform: translateX(0);
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
.header {
|
| 31 |
padding: 1rem;
|
| 32 |
-
border-bottom: 1px solid #374151;
|
| 33 |
-
display: flex;
|
| 34 |
-
justify-content: space-between;
|
| 35 |
-
align-items: center;
|
| 36 |
-
background: #1f2937;
|
| 37 |
}
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
.timeline-hour {
|
| 44 |
-
position: relative;
|
| 45 |
-
min-height: 60px;
|
| 46 |
border-bottom: 1px solid #374151;
|
| 47 |
-
padding-left: 50px;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
.hour-label {
|
| 51 |
-
position: absolute;
|
| 52 |
-
left: 10px;
|
| 53 |
-
top: 5px;
|
| 54 |
-
font-size: 12px;
|
| 55 |
-
color: #9ca3af;
|
| 56 |
}
|
| 57 |
-
|
| 58 |
-
.task-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
transition: background-color 0.2s ease;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
.task-drop-area:hover {
|
| 66 |
-
background-color: #37415170;
|
| 67 |
}
|
| 68 |
-
|
| 69 |
-
.task-
|
| 70 |
background: #374151;
|
| 71 |
-
border-
|
| 72 |
-
padding:
|
| 73 |
-
|
| 74 |
-
border-radius: 4px;
|
| 75 |
-
font-size: 14px;
|
| 76 |
-
cursor: pointer;
|
| 77 |
-
margin-bottom: 4px;
|
| 78 |
transition: all 0.2s ease;
|
| 79 |
}
|
| 80 |
-
|
| 81 |
-
.task-
|
| 82 |
background: #4b5563;
|
| 83 |
-
transform:
|
| 84 |
}
|
| 85 |
-
|
| 86 |
-
.
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
top: 50%;
|
| 90 |
-
transform: translateY(-50%);
|
| 91 |
-
background: #1f2937;
|
| 92 |
-
border: 1px solid #374151;
|
| 93 |
-
border-right: none;
|
| 94 |
-
padding: 1rem 0.5rem;
|
| 95 |
-
border-radius: 5px 0 0 5px;
|
| 96 |
-
cursor: pointer;
|
| 97 |
-
z-index: 30;
|
| 98 |
-
color: white;
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
.toggle-btn:hover {
|
| 102 |
-
background: #374151;
|
| 103 |
}
|
| 104 |
-
|
| 105 |
-
.
|
| 106 |
-
|
| 107 |
-
|
| 108 |
}
|
| 109 |
</style>
|
| 110 |
-
|
| 111 |
-
<
|
| 112 |
-
|
| 113 |
-
</button>
|
| 114 |
-
|
| 115 |
-
<div class="header">
|
| 116 |
-
<h3>Timeline</h3>
|
| 117 |
-
<button id="close-timeline">
|
| 118 |
-
<i data-feather="x"></i>
|
| 119 |
-
</button>
|
| 120 |
-
</div>
|
| 121 |
-
|
| 122 |
-
<div class="timeline-container" id="timeline-hours">
|
| 123 |
-
${Array.from({ length: 24 }, (_, i) => `
|
| 124 |
-
<div class="timeline-hour" data-hour="${i}">
|
| 125 |
-
<span class="hour-label">${i.toString().padStart(2, '0')}:00</span>
|
| 126 |
-
<div class="task-drop-area" data-hour="${i}"></div>
|
| 127 |
-
</div>
|
| 128 |
-
`).join('')}
|
| 129 |
-
</div>
|
| 130 |
`;
|
| 131 |
}
|
| 132 |
|
| 133 |
-
|
| 134 |
-
const
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
});
|
| 142 |
-
|
| 143 |
-
// Setup drag and drop
|
| 144 |
-
this.shadowRoot.addEventListener('dragover', (e) => {
|
| 145 |
-
if (e.target.classList.contains('task-drop-area')) {
|
| 146 |
-
e.preventDefault();
|
| 147 |
-
e.target.style.backgroundColor = '#37415170';
|
| 148 |
-
}
|
| 149 |
-
});
|
| 150 |
-
|
| 151 |
-
this.shadowRoot.addEventListener('dragleave', (e) => {
|
| 152 |
-
if (e.target.classList.contains('task-drop-area')) {
|
| 153 |
-
e.target.style.backgroundColor = '';
|
| 154 |
-
}
|
| 155 |
-
});
|
| 156 |
-
|
| 157 |
-
this.shadowRoot.addEventListener('drop', (e) => {
|
| 158 |
-
if (e.target.classList.contains('task-drop-area')) {
|
| 159 |
-
e.preventDefault();
|
| 160 |
-
e.target.style.backgroundColor = '';
|
| 161 |
-
|
| 162 |
-
const taskId = e.dataTransfer.getData('text/plain');
|
| 163 |
-
const hour = parseInt(e.target.dataset.hour);
|
| 164 |
-
|
| 165 |
-
this.dispatchEvent(new CustomEvent('task-dropped', {
|
| 166 |
-
detail: { taskId, hour }
|
| 167 |
-
}));
|
| 168 |
-
}
|
| 169 |
-
});
|
| 170 |
-
|
| 171 |
-
// Initialize timeline
|
| 172 |
-
setTimeout(() => {
|
| 173 |
-
this.updateTimeline();
|
| 174 |
-
}, 100);
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
updateTimeline() {
|
| 178 |
-
const today = new Date().toDateString();
|
| 179 |
-
const scheduledTasks = state.timelineTasks.filter(t =>
|
| 180 |
-
new Date(t.date).toDateString() === today
|
| 181 |
-
);
|
| 182 |
-
|
| 183 |
-
const container = this.shadowRoot.querySelector('.timeline-container');
|
| 184 |
-
container.querySelectorAll('.task-block').forEach(el => el.remove());
|
| 185 |
-
|
| 186 |
-
scheduledTasks.forEach(timelineTask => {
|
| 187 |
-
const task = state.tasks.find(t => t.id === timelineTask.taskId);
|
| 188 |
-
if (!task || task.completed) return;
|
| 189 |
-
|
| 190 |
-
const startTime = new Date(timelineTask.startTime);
|
| 191 |
-
const hour = startTime.getHours();
|
| 192 |
-
const minute = startTime.getMinutes();
|
| 193 |
-
|
| 194 |
-
const hourEl = this.shadowRoot.querySelector(`.timeline-hour[data-hour="${hour}"]`);
|
| 195 |
-
if (hourEl) {
|
| 196 |
-
const taskEl = document.createElement('div');
|
| 197 |
-
taskEl.className = 'task-block';
|
| 198 |
-
taskEl.innerHTML = `
|
| 199 |
-
<div class="task-title">${task.title}</div>
|
| 200 |
-
<div class="task-time">${formatTime(task.estimatedTime)}</div>
|
| 201 |
-
`;
|
| 202 |
-
taskEl.draggable = true;
|
| 203 |
-
taskEl.dataset.taskId = task.id;
|
| 204 |
-
|
| 205 |
-
hourEl.appendChild(taskEl);
|
| 206 |
-
}
|
| 207 |
-
});
|
| 208 |
}
|
| 209 |
}
|
| 210 |
|
| 211 |
-
customElements.define('timeline-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class TimelineTasks extends HTMLElement {
|
| 2 |
connectedCallback() {
|
| 3 |
this.attachShadow({ mode: 'open' });
|
| 4 |
this.render();
|
|
|
|
| 5 |
}
|
| 6 |
|
| 7 |
render() {
|
|
|
|
| 9 |
<style>
|
| 10 |
:host {
|
| 11 |
display: block;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
background: #1f2937;
|
| 13 |
border-left: 1px solid #374151;
|
| 14 |
+
height: 100%;
|
| 15 |
+
width: 300px;
|
| 16 |
+
position: fixed;
|
| 17 |
+
right: 0;
|
| 18 |
+
top: 80px;
|
| 19 |
overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
padding: 1rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
| 22 |
+
|
| 23 |
+
h3 {
|
| 24 |
+
margin-top: 0;
|
| 25 |
+
color: #f97316;
|
| 26 |
+
padding-bottom: 1rem;
|
|
|
|
|
|
|
|
|
|
| 27 |
border-bottom: 1px solid #374151;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
+
|
| 30 |
+
.task-list {
|
| 31 |
+
display: flex;
|
| 32 |
+
flex-direction: column;
|
| 33 |
+
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
+
|
| 36 |
+
.task-item {
|
| 37 |
background: #374151;
|
| 38 |
+
border-radius: 6px;
|
| 39 |
+
padding: 8px;
|
| 40 |
+
cursor: grab;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
transition: all 0.2s ease;
|
| 42 |
}
|
| 43 |
+
|
| 44 |
+
.task-item:hover {
|
| 45 |
background: #4b5563;
|
| 46 |
+
transform: translateX(-4px);
|
| 47 |
}
|
| 48 |
+
|
| 49 |
+
.task-title {
|
| 50 |
+
font-size: 14px;
|
| 51 |
+
margin-bottom: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
+
|
| 54 |
+
.task-time {
|
| 55 |
+
font-size: 12px;
|
| 56 |
+
color: #9ca3af;
|
| 57 |
}
|
| 58 |
</style>
|
| 59 |
+
|
| 60 |
+
<h3>Timeline</h3>
|
| 61 |
+
<div class="task-list" id="timeline-task-list"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
`;
|
| 63 |
}
|
| 64 |
|
| 65 |
+
updateTasks(tasks) {
|
| 66 |
+
const container = this.shadowRoot.getElementById('timeline-task-list');
|
| 67 |
+
container.innerHTML = tasks.map(task => `
|
| 68 |
+
<div class="task-item" draggable="true" data-task-id="${task.id}">
|
| 69 |
+
<div class="task-title">${task.title}</div>
|
| 70 |
+
<div class="task-time">${formatTime(task.estimatedTime)}</div>
|
| 71 |
+
</div>
|
| 72 |
+
`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
}
|
| 75 |
|
| 76 |
+
customElements.define('timeline-tasks', TimelineTasks);
|
| 77 |
+
|
| 78 |
+
function formatTime(minutes) {
|
| 79 |
+
const h = Math.floor(minutes / 60);
|
| 80 |
+
const m = minutes % 60;
|
| 81 |
+
return `${h}h ${m}m`;
|
| 82 |
+
}
|
index.html
CHANGED
|
@@ -1,824 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
isBreak: false,
|
| 12 |
-
timeRemaining: 0,
|
| 13 |
-
totalTime: 0,
|
| 14 |
-
interval: null
|
| 15 |
-
},
|
| 16 |
-
timelineTasks: []
|
| 17 |
-
};
|
| 18 |
-
// Initialize
|
| 19 |
-
document.addEventListener('DOMContentLoaded', function() {
|
| 20 |
-
loadFromLocalStorage();
|
| 21 |
-
initializeUI();
|
| 22 |
-
setupEventListeners();
|
| 23 |
-
render();
|
| 24 |
-
});
|
| 25 |
-
// Local Storage
|
| 26 |
-
function loadFromLocalStorage() {
|
| 27 |
-
const savedTasks = localStorage.getItem('tasks');
|
| 28 |
-
const savedTags = localStorage.getItem('tags');
|
| 29 |
-
const savedTimeline = localStorage.getItem('timeline');
|
| 30 |
-
|
| 31 |
-
if (savedTasks) {
|
| 32 |
-
state.tasks = JSON.parse(savedTasks);
|
| 33 |
-
}
|
| 34 |
-
if (savedTags) {
|
| 35 |
-
state.tags = JSON.parse(savedTags);
|
| 36 |
-
}
|
| 37 |
-
if (savedTimeline) {
|
| 38 |
-
state.timelineTasks = JSON.parse(savedTimeline);
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
// Load Pomodoro settings
|
| 42 |
-
const workTime = localStorage.getItem('pomodoroWorkTime') || 25;
|
| 43 |
-
const breakTime = localStorage.getItem('pomodoroBreakTime') || 5;
|
| 44 |
-
document.getElementById('pomodoro-work').value = workTime;
|
| 45 |
-
document.getElementById('pomodoro-break').value = breakTime;
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
function saveToLocalStorage() {
|
| 49 |
-
localStorage.setItem('tasks', JSON.stringify(state.tasks));
|
| 50 |
-
localStorage.setItem('tags', JSON.stringify(state.tags));
|
| 51 |
-
localStorage.setItem('timeline', JSON.stringify(state.timelineTasks));
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
// UI Initialization
|
| 55 |
-
function initializeUI() {
|
| 56 |
-
// Current time display
|
| 57 |
-
updateCurrentTime();
|
| 58 |
-
setInterval(updateCurrentTime, 1000);
|
| 59 |
-
|
| 60 |
-
// Initialize timeline
|
| 61 |
-
initializeTimeline();
|
| 62 |
-
|
| 63 |
-
// Scroll to current time in timeline
|
| 64 |
-
setTimeout(scrollToCurrentTime, 100);
|
| 65 |
-
// Check for default tags
|
| 66 |
-
if (state.tags.length === 0) {
|
| 67 |
-
state.tags = [
|
| 68 |
-
{ id: '1', name: 'Wichtig', color: '#ef4444' },
|
| 69 |
-
{ id: '2', name: 'Privat', color: '#10b981' },
|
| 70 |
-
{ id: '3', name: 'Arbeit', color: '#3b82f6' }
|
| 71 |
-
];
|
| 72 |
-
saveToLocalStorage();
|
| 73 |
-
}
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
function updateCurrentTime() {
|
| 77 |
-
const now = new Date();
|
| 78 |
-
const timeString = now.toLocaleTimeString('de-DE', {
|
| 79 |
-
hour: '2-digit',
|
| 80 |
-
minute: '2-digit'
|
| 81 |
-
});
|
| 82 |
-
document.getElementById('current-time').textContent = timeString;
|
| 83 |
-
}
|
| 84 |
-
function setupEventListeners() {
|
| 85 |
-
// Navigation
|
| 86 |
-
document.querySelectorAll('.nav-btn').forEach(btn => {
|
| 87 |
-
btn.addEventListener('click', (e) => {
|
| 88 |
-
const view = e.currentTarget.dataset.view;
|
| 89 |
-
switchView(view);
|
| 90 |
-
});
|
| 91 |
-
});
|
| 92 |
-
|
| 93 |
-
// FAB buttons
|
| 94 |
-
document.getElementById('add-task-fab').addEventListener('click', openAddTaskModal);
|
| 95 |
-
document.getElementById('focus-fab').addEventListener('click', enterFocusMode);
|
| 96 |
-
|
| 97 |
-
// Add task modal
|
| 98 |
-
document.getElementById('add-task-form').addEventListener('submit', handleAddTask);
|
| 99 |
-
document.getElementById('cancel-task').addEventListener('click', closeAddTaskModal);
|
| 100 |
-
|
| 101 |
-
// Settings
|
| 102 |
-
document.getElementById('import-btn').addEventListener('click', handleImport);
|
| 103 |
-
document.getElementById('export-btn').addEventListener('click', handleExport);
|
| 104 |
-
document.getElementById('add-tag-btn').addEventListener('click', handleAddTag);
|
| 105 |
-
|
| 106 |
-
document.getElementById('pomodoro-work').addEventListener('change', savePomodoroSettings);
|
| 107 |
-
document.getElementById('pomodoro-break').addEventListener('change', savePomodoroSettings);
|
| 108 |
-
|
| 109 |
-
// Focus mode
|
| 110 |
-
document.getElementById('focus-pause').addEventListener('click', pausePomodoro);
|
| 111 |
-
document.getElementById('focus-play').addEventListener('click', resumePomodoro);
|
| 112 |
-
document.getElementById('focus-stop').addEventListener('click', stopPomodoro);
|
| 113 |
-
|
| 114 |
-
// Event delegation for task item events
|
| 115 |
-
document.addEventListener('delete-task', (e) => {
|
| 116 |
-
deleteTask(e.detail.taskId);
|
| 117 |
-
});
|
| 118 |
-
|
| 119 |
-
document.addEventListener('toggle-complete', (e) => {
|
| 120 |
-
toggleTaskComplete(e.detail.taskId, e.detail.completed);
|
| 121 |
-
});
|
| 122 |
-
|
| 123 |
-
document.addEventListener('update-task', (e) => {
|
| 124 |
-
updateTask(e.detail.taskId, e.detail.updates);
|
| 125 |
-
});
|
| 126 |
-
|
| 127 |
-
document.addEventListener('add-time', (e) => {
|
| 128 |
-
addTimeToTask(e.detail.taskId, e.detail.minutes);
|
| 129 |
-
});
|
| 130 |
-
|
| 131 |
-
// Timeline sidebar events
|
| 132 |
-
const timelineSidebar = document.getElementById('timeline-sidebar');
|
| 133 |
-
if (timelineSidebar) {
|
| 134 |
-
timelineSidebar.addEventListener('task-dropped', (e) => {
|
| 135 |
-
scheduleTask(e.detail.taskId, e.detail.hour, 0);
|
| 136 |
-
});
|
| 137 |
-
}
|
| 138 |
-
}
|
| 139 |
-
// View Management
|
| 140 |
-
function switchView(view) {
|
| 141 |
-
state.currentView = view;
|
| 142 |
-
|
| 143 |
-
// Update nav buttons
|
| 144 |
-
document.querySelectorAll('.nav-btn').forEach(btn => {
|
| 145 |
-
btn.classList.toggle('active', btn.dataset.view === view);
|
| 146 |
-
});
|
| 147 |
-
|
| 148 |
-
// Hide all views first
|
| 149 |
-
document.querySelectorAll('.view').forEach(v => {
|
| 150 |
-
v.classList.remove('active');
|
| 151 |
-
v.style.display = 'none';
|
| 152 |
-
});
|
| 153 |
-
|
| 154 |
-
// Show only the active view
|
| 155 |
-
const activeView = document.getElementById(`${view}-view`);
|
| 156 |
-
if (activeView) {
|
| 157 |
-
activeView.style.display = 'block';
|
| 158 |
-
activeView.classList.add('active');
|
| 159 |
-
}
|
| 160 |
-
}
|
| 161 |
-
// Task Management
|
| 162 |
-
function addTask(task) {
|
| 163 |
-
const newTask = {
|
| 164 |
-
id: Date.now().toString(),
|
| 165 |
-
title: task.title,
|
| 166 |
-
completed: false,
|
| 167 |
-
estimatedTime: task.estimatedTime, // in minutes
|
| 168 |
-
spentTime: 0, // in minutes
|
| 169 |
-
tags: task.tags || [],
|
| 170 |
-
createdAt: new Date().toISOString()
|
| 171 |
-
};
|
| 172 |
-
|
| 173 |
-
state.tasks.push(newTask);
|
| 174 |
-
saveToLocalStorage();
|
| 175 |
-
render();
|
| 176 |
-
|
| 177 |
-
return newTask;
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
function updateTask(taskId, updates) {
|
| 181 |
-
const task = state.tasks.find(t => t.id === taskId);
|
| 182 |
-
if (task) {
|
| 183 |
-
Object.assign(task, updates);
|
| 184 |
-
saveToLocalStorage();
|
| 185 |
-
render();
|
| 186 |
-
}
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
function deleteTask(taskId) {
|
| 190 |
-
state.tasks = state.tasks.filter(t => t.id !== taskId);
|
| 191 |
-
state.timelineTasks = state.timelineTasks.filter(t => t.taskId !== taskId);
|
| 192 |
-
saveToLocalStorage();
|
| 193 |
-
render();
|
| 194 |
-
}
|
| 195 |
-
function toggleTaskComplete(taskId, completed) {
|
| 196 |
-
const task = state.tasks.find(t => t.id === taskId);
|
| 197 |
-
if (task) {
|
| 198 |
-
task.completed = completed !== undefined ? completed : !task.completed;
|
| 199 |
-
saveToLocalStorage();
|
| 200 |
-
render();
|
| 201 |
-
}
|
| 202 |
-
}
|
| 203 |
-
// Modal Functions
|
| 204 |
-
function openAddTaskModal() {
|
| 205 |
-
const modal = document.getElementById('add-task-modal');
|
| 206 |
-
modal.classList.remove('hidden');
|
| 207 |
-
renderModalTags();
|
| 208 |
-
document.getElementById('task-title').focus();
|
| 209 |
-
}
|
| 210 |
-
function closeAddTaskModal() {
|
| 211 |
-
const modal = document.getElementById('add-task-modal');
|
| 212 |
-
modal.classList.add('hidden');
|
| 213 |
-
document.getElementById('add-task-form').reset();
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
function handleAddTask(e) {
|
| 217 |
-
e.preventDefault();
|
| 218 |
-
|
| 219 |
-
const title = document.getElementById('task-title').value;
|
| 220 |
-
const hours = parseInt(document.getElementById('task-hours').value) || 0;
|
| 221 |
-
const minutes = parseInt(document.getElementById('task-minutes').value) || 0;
|
| 222 |
-
const estimatedTime = hours * 60 + minutes;
|
| 223 |
-
|
| 224 |
-
const selectedTags = Array.from(document.querySelectorAll('#modal-tags-list input:checked')).map(cb => cb.value);
|
| 225 |
-
|
| 226 |
-
addTask({
|
| 227 |
-
title,
|
| 228 |
-
estimatedTime,
|
| 229 |
-
tags: selectedTags
|
| 230 |
-
});
|
| 231 |
-
|
| 232 |
-
closeAddTaskModal();
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
function renderModalTags() {
|
| 236 |
-
const container = document.getElementById('modal-tags-list');
|
| 237 |
-
container.innerHTML = state.tags.map(tag => `
|
| 238 |
-
<label class="flex items-center space-x-2 cursor-pointer">
|
| 239 |
-
<input type="checkbox" value="${tag.id}" class="custom-checkbox">
|
| 240 |
-
<span class="tag-badge" style="background-color: ${tag.color}20; color: ${tag.color}">
|
| 241 |
-
${tag.name}
|
| 242 |
-
</span>
|
| 243 |
-
</label>
|
| 244 |
-
`).join('');
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
// Tag Management
|
| 248 |
-
function addTag(tag) {
|
| 249 |
-
state.tags.push({
|
| 250 |
-
id: Date.now().toString(),
|
| 251 |
-
name: tag.name,
|
| 252 |
-
color: tag.color
|
| 253 |
-
});
|
| 254 |
-
saveToLocalStorage();
|
| 255 |
-
render();
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
function deleteTag(tagId) {
|
| 259 |
-
state.tags = state.tags.filter(t => t.id !== tagId);
|
| 260 |
-
// Remove tag from all tasks
|
| 261 |
-
state.tasks.forEach(task => {
|
| 262 |
-
task.tags = task.tags.filter(t => t !== tagId);
|
| 263 |
-
});
|
| 264 |
-
saveToLocalStorage();
|
| 265 |
-
render();
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
function handleAddTag() {
|
| 269 |
-
const name = document.getElementById('new-tag-name').value;
|
| 270 |
-
const color = document.getElementById('new-tag-color').value;
|
| 271 |
-
|
| 272 |
-
if (name) {
|
| 273 |
-
addTag({ name, color });
|
| 274 |
-
document.getElementById('new-tag-name').value = '';
|
| 275 |
-
}
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
// Import/Export
|
| 279 |
-
function handleImport() {
|
| 280 |
-
const markdown = document.getElementById('import-md').value;
|
| 281 |
-
if (!markdown) return;
|
| 282 |
-
|
| 283 |
-
const tasks = parseMarkdown(markdown);
|
| 284 |
-
tasks.forEach(task => {
|
| 285 |
-
// Check if task already exists
|
| 286 |
-
const exists = state.tasks.some(t => t.title === task.title);
|
| 287 |
-
if (!exists) {
|
| 288 |
-
state.tasks.push({
|
| 289 |
-
id: Date.now().toString() + Math.random(),
|
| 290 |
-
title: task.title,
|
| 291 |
-
completed: task.completed,
|
| 292 |
-
estimatedTime: task.estimatedTime,
|
| 293 |
-
spentTime: task.spentTime || 0,
|
| 294 |
-
tags: [],
|
| 295 |
-
createdAt: new Date().toISOString()
|
| 296 |
-
});
|
| 297 |
-
}
|
| 298 |
-
});
|
| 299 |
-
|
| 300 |
-
saveToLocalStorage();
|
| 301 |
-
render();
|
| 302 |
-
document.getElementById('import-md').value = '';
|
| 303 |
-
}
|
| 304 |
-
|
| 305 |
-
function handleExport() {
|
| 306 |
-
const markdown = generateMarkdown();
|
| 307 |
-
const blob = new Blob([markdown], { type: 'text/markdown' });
|
| 308 |
-
const url = URL.createObjectURL(blob);
|
| 309 |
-
const a = document.createElement('a');
|
| 310 |
-
a.href = url;
|
| 311 |
-
a.download = 'tasks.md';
|
| 312 |
-
a.click();
|
| 313 |
-
URL.revokeObjectURL(url);
|
| 314 |
-
}
|
| 315 |
-
function parseMarkdown(markdown) {
|
| 316 |
-
const lines = markdown.split('\n');
|
| 317 |
-
const tasks = [];
|
| 318 |
-
|
| 319 |
-
const taskRegex = /^\s*-\s*\[([ x])\]\s+(.+?)(?:\s*\|\s*(.+?)(?:\s*\|\s*(.+?))?)?\s*$/;
|
| 320 |
-
|
| 321 |
-
lines.forEach(line => {
|
| 322 |
-
const match = line.match(taskRegex);
|
| 323 |
-
if (match) {
|
| 324 |
-
const completed = match[1] === 'x';
|
| 325 |
-
const title = match[2].trim();
|
| 326 |
-
const timePart = match[3] ? match[3].trim() : '0h';
|
| 327 |
-
const tagsPart = match[4] ? match[4].trim() : '';
|
| 328 |
-
|
| 329 |
-
// Parse time
|
| 330 |
-
const timeMatch = timePart.match(/(\d+)(?:\s*h\s*)?(?:(?:\s*(\d+)\s*m\s*)?)?/);
|
| 331 |
-
const hours = parseInt(timeMatch[1]) || 0;
|
| 332 |
-
const minutes = parseInt(timeMatch[2]) || 0;
|
| 333 |
-
const estimatedTime = hours * 60 + minutes;
|
| 334 |
-
|
| 335 |
-
tasks.push({
|
| 336 |
-
title,
|
| 337 |
-
completed,
|
| 338 |
-
estimatedTime
|
| 339 |
-
});
|
| 340 |
-
}
|
| 341 |
-
});
|
| 342 |
-
return tasks;
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
function generateMarkdown() {
|
| 346 |
-
return state.tasks.map(task => {
|
| 347 |
-
const status = task.completed ? 'x' : ' ';
|
| 348 |
-
const hours = Math.floor(task.estimatedTime / 60);
|
| 349 |
-
const minutes = task.estimatedTime % 60;
|
| 350 |
-
const timeStr = `${hours}h${minutes > 0 ? ` ${minutes}m` : ''}`;
|
| 351 |
-
return `- [${status}] ${task.title} | ${timeStr}`;
|
| 352 |
-
}).join('\n');
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
// Rendering
|
| 356 |
-
function render() {
|
| 357 |
-
renderTasks();
|
| 358 |
-
renderProgressBars();
|
| 359 |
-
renderTags();
|
| 360 |
-
renderTimeline();
|
| 361 |
-
}
|
| 362 |
-
function renderTasks() {
|
| 363 |
-
const openContainer = document.getElementById('open-tasks');
|
| 364 |
-
const completedContainer = document.getElementById('completed-tasks');
|
| 365 |
-
|
| 366 |
-
const openTasks = state.tasks.filter(t => !t.completed);
|
| 367 |
-
const completedTasks = state.tasks.filter(t => t.completed);
|
| 368 |
-
|
| 369 |
-
// Render open tasks
|
| 370 |
-
if (openTasks.length === 0) {
|
| 371 |
-
openContainer.innerHTML = '<div class="empty-state"><i data-feather="inbox"></i><p>Keine offenen Aufgaben</p></div>';
|
| 372 |
-
} else {
|
| 373 |
-
openContainer.innerHTML = '';
|
| 374 |
-
openTasks.forEach(task => {
|
| 375 |
-
const taskElement = document.createElement('task-item');
|
| 376 |
-
taskElement.setAttribute('task-id', task.id);
|
| 377 |
-
taskElement.setAttribute('completed', 'false');
|
| 378 |
-
openContainer.appendChild(taskElement);
|
| 379 |
-
});
|
| 380 |
-
}
|
| 381 |
-
|
| 382 |
-
// Render completed tasks
|
| 383 |
-
if (completedTasks.length === 0) {
|
| 384 |
-
completedContainer.innerHTML = '<div class="empty-state"><i data-feather="check-circle"></i><p>Keine erledigten Aufgaben</p></div>';
|
| 385 |
-
} else {
|
| 386 |
-
completedContainer.innerHTML = '';
|
| 387 |
-
completedTasks.forEach(task => {
|
| 388 |
-
const taskElement = document.createElement('task-item');
|
| 389 |
-
taskElement.setAttribute('task-id', task.id);
|
| 390 |
-
taskElement.setAttribute('completed', 'true');
|
| 391 |
-
completedContainer.appendChild(taskElement);
|
| 392 |
-
});
|
| 393 |
-
}
|
| 394 |
-
|
| 395 |
-
document.getElementById('open-count').textContent = openTasks.length;
|
| 396 |
-
document.getElementById('completed-count').textContent = completedTasks.length;
|
| 397 |
-
|
| 398 |
-
// Setup drag and drop for task items
|
| 399 |
-
setupTaskDragAndDrop();
|
| 400 |
-
|
| 401 |
-
// Replace feather icons
|
| 402 |
-
feather.replace();
|
| 403 |
-
}
|
| 404 |
-
function renderProgressBars() {
|
| 405 |
-
// Overall progress
|
| 406 |
-
const totalEstimated = state.tasks.reduce((sum, t) => sum + t.estimatedTime, 0) || 1;
|
| 407 |
-
const totalSpent = state.tasks.reduce((sum, t) => sum + t.spentTime, 0);
|
| 408 |
-
const totalProgress = (totalSpent / totalEstimated) * 100;
|
| 409 |
-
|
| 410 |
-
document.getElementById('total-progress-bar').style.width = `${totalProgress}%`;
|
| 411 |
-
document.getElementById('total-progress-text').textContent =
|
| 412 |
-
`${Math.round(totalProgress)}% (${formatTime(totalSpent)} / ${formatTime(totalEstimated)})`;
|
| 413 |
-
|
| 414 |
-
// Tag progress bars - only for tags with tasks
|
| 415 |
-
const container = document.getElementById('tag-progress-container');
|
| 416 |
-
container.innerHTML = state.tags
|
| 417 |
-
.filter(tag => state.tasks.some(t => t.tags.includes(tag.id))) // Only tags with tasks
|
| 418 |
-
.map(tag => {
|
| 419 |
-
const tagTasks = state.tasks.filter(t => t.tags.includes(tag.id));
|
| 420 |
-
const tagEstimated = tagTasks.reduce((sum, t) => sum + t.estimatedTime, 0) || 1;
|
| 421 |
-
const tagSpent = tagTasks.reduce((sum, t) => sum + t.spentTime, 0);
|
| 422 |
-
const tagProgress = (tagSpent / tagEstimated) * 100;
|
| 423 |
-
|
| 424 |
-
return `
|
| 425 |
-
<div class="bg-gray-800 rounded-lg p-4 shadow transition-all hover:scale-[1.02]">
|
| 426 |
-
<div class="flex items-center justify-between mb-2">
|
| 427 |
-
<span class="font-medium" style="color: ${tag.color}">${tag.name}</span>
|
| 428 |
-
<span class="text-sm text-gray-400">
|
| 429 |
-
${Math.round(tagProgress)}% (${formatTime(tagSpent)} / ${formatTime(tagEstimated)})
|
| 430 |
-
</span>
|
| 431 |
-
</div>
|
| 432 |
-
<div class="w-full bg-gray-700 rounded-full h-2 overflow-hidden">
|
| 433 |
-
<div class="h-full rounded-full transition-all duration-500"
|
| 434 |
-
style="width: ${tagProgress}%; background-color: ${tag.color}"></div>
|
| 435 |
</div>
|
|
|
|
| 436 |
</div>
|
| 437 |
-
`;
|
| 438 |
-
}).join('');
|
| 439 |
-
}
|
| 440 |
-
function renderTags() {
|
| 441 |
-
const container = document.getElementById('tags-list');
|
| 442 |
-
container.innerHTML = state.tags.map(tag => `
|
| 443 |
-
<tag-item tag-id="${tag.id}" tag-name="${tag.name}" tag-color="${tag.color}"></tag-item>
|
| 444 |
-
`).join('');
|
| 445 |
-
|
| 446 |
-
// Render tag components
|
| 447 |
-
document.querySelectorAll('tag-item').forEach(el => {
|
| 448 |
-
const tagId = el.getAttribute('tag-id');
|
| 449 |
-
el.addEventListener('delete', () => deleteTag(tagId));
|
| 450 |
-
});
|
| 451 |
-
}
|
| 452 |
-
function highlightCurrentHour() {
|
| 453 |
-
const now = new Date();
|
| 454 |
-
const currentHour = now.getHours();
|
| 455 |
-
const currentMinute = now.getMinutes();
|
| 456 |
-
|
| 457 |
-
// Highlight current hour
|
| 458 |
-
document.querySelectorAll('.timeline-hour').forEach(el => {
|
| 459 |
-
el.classList.remove('bg-gray-750');
|
| 460 |
-
});
|
| 461 |
-
|
| 462 |
-
const hourElement = document.querySelector(`.timeline-hour[data-hour="${currentHour}"]`);
|
| 463 |
-
if (hourElement) {
|
| 464 |
-
hourElement.classList.add('bg-gray-750');
|
| 465 |
-
}
|
| 466 |
-
|
| 467 |
-
// Add current time indicator line
|
| 468 |
-
document.getElementById('current-time-indicator')?.remove();
|
| 469 |
-
|
| 470 |
-
const timelineContainer = document.getElementById('timeline-hours');
|
| 471 |
-
if (timelineContainer) {
|
| 472 |
-
const indicator = document.createElement('div');
|
| 473 |
-
indicator.id = 'current-time-indicator';
|
| 474 |
-
indicator.style.position = 'absolute';
|
| 475 |
-
indicator.style.left = '0';
|
| 476 |
-
indicator.style.right = '0';
|
| 477 |
-
indicator.style.height = '2px';
|
| 478 |
-
indicator.style.backgroundColor = '#ef4444';
|
| 479 |
-
indicator.style.zIndex = '10';
|
| 480 |
-
|
| 481 |
-
const hourElement = document.querySelector(`.timeline-hour[data-hour="${currentHour}"]`);
|
| 482 |
-
if (hourElement) {
|
| 483 |
-
const hourTop = hourElement.offsetTop;
|
| 484 |
-
const hourHeight = hourElement.offsetHeight;
|
| 485 |
-
const minutePosition = (currentMinute / 60) * hourHeight;
|
| 486 |
-
|
| 487 |
-
indicator.style.top = `${hourTop + minutePosition}px`;
|
| 488 |
-
timelineContainer.appendChild(indicator);
|
| 489 |
-
|
| 490 |
-
// Add time label
|
| 491 |
-
const timeLabel = document.createElement('div');
|
| 492 |
-
timeLabel.textContent = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
| 493 |
-
timeLabel.style.position = 'absolute';
|
| 494 |
-
timeLabel.style.left = '-64px';
|
| 495 |
-
timeLabel.style.top = `${hourTop + minutePosition - 10}px`;
|
| 496 |
-
timeLabel.style.backgroundColor = '#ef4444';
|
| 497 |
-
timeLabel.style.color = 'white';
|
| 498 |
-
timeLabel.style.padding = '2px 8px';
|
| 499 |
-
timeLabel.style.borderRadius = '4px';
|
| 500 |
-
timeLabel.style.fontSize = '12px';
|
| 501 |
-
timeLabel.style.zIndex = '10';
|
| 502 |
-
|
| 503 |
-
hourEl.appendChild(timeLabel);
|
| 504 |
-
}
|
| 505 |
-
}
|
| 506 |
-
}
|
| 507 |
-
function scrollToCurrentTime() {
|
| 508 |
-
const now = new Date();
|
| 509 |
-
const currentHour = now.getHours();
|
| 510 |
-
const hourEl = document.querySelector(`[data-hour="${currentHour}"]`);
|
| 511 |
-
|
| 512 |
-
if (hourEl) {
|
| 513 |
-
hourEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 514 |
-
}
|
| 515 |
-
}
|
| 516 |
-
|
| 517 |
-
// Focus Mode & Pomodoro
|
| 518 |
-
function enterFocusMode() {
|
| 519 |
-
// Open a task selection modal
|
| 520 |
-
const openTasks = state.tasks.filter(t => !t.completed);
|
| 521 |
-
if (openTasks.length === 0) {
|
| 522 |
-
alert('Keine offenen Aufgaben für den Fokus-Modus!');
|
| 523 |
-
return;
|
| 524 |
-
}
|
| 525 |
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
<div class="bg-gray-700 p-3 rounded-lg cursor-pointer hover:bg-gray-600 transition"
|
| 534 |
-
onclick="startFocusModeWithId('${task.id}')">
|
| 535 |
-
<div class="font-medium">${task.title}</div>
|
| 536 |
-
<div class="text-sm text-gray-400">${formatTime(task.estimatedTime)}</div>
|
| 537 |
</div>
|
| 538 |
-
|
|
|
|
| 539 |
</div>
|
| 540 |
-
<button onclick="this.closest('div[class*=\"fixed\"]').remove()"
|
| 541 |
-
class="mt-4 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition w-full">
|
| 542 |
-
Abbrechen
|
| 543 |
-
</button>
|
| 544 |
</div>
|
| 545 |
-
`;
|
| 546 |
-
document.body.appendChild(modal);
|
| 547 |
-
}
|
| 548 |
-
|
| 549 |
-
function startFocusModeWithId(taskId) {
|
| 550 |
-
const task = state.tasks.find(t => t.id === taskId);
|
| 551 |
-
if (task) {
|
| 552 |
-
document.querySelector('div[class*="fixed"]')?.remove();
|
| 553 |
-
startFocusMode(task);
|
| 554 |
-
}
|
| 555 |
-
}
|
| 556 |
-
function startFocusMode(task) {
|
| 557 |
-
state.focusTask = task;
|
| 558 |
-
document.getElementById('focus-task-title').textContent = task.title;
|
| 559 |
-
document.getElementById('focus-mode').classList.remove('hidden');
|
| 560 |
-
|
| 561 |
-
// Start pomodoro
|
| 562 |
-
const workTime = parseInt(localStorage.getItem('pomodoroWorkTime')) || 25;
|
| 563 |
-
startPomodoro(workTime * 60, false);
|
| 564 |
-
}
|
| 565 |
-
function startPomodoro(seconds, isBreak) {
|
| 566 |
-
state.pomodoro.isRunning = true;
|
| 567 |
-
state.pomodoro.isBreak = isBreak;
|
| 568 |
-
state.pomodoro.timeRemaining = seconds;
|
| 569 |
-
state.pomodoro.totalTime = seconds;
|
| 570 |
-
|
| 571 |
-
clearInterval(state.pomodoro.interval);
|
| 572 |
-
|
| 573 |
-
state.pomodoro.interval = setInterval(() => {
|
| 574 |
-
if (state.pomodoro.timeRemaining > 0) {
|
| 575 |
-
state.pomodoro.timeRemaining--;
|
| 576 |
-
updatePomodoroDisplay();
|
| 577 |
-
} else {
|
| 578 |
-
handlePomodoroComplete();
|
| 579 |
-
}
|
| 580 |
-
}, 1000);
|
| 581 |
-
|
| 582 |
-
updatePomodoroDisplay();
|
| 583 |
-
}
|
| 584 |
-
|
| 585 |
-
function updatePomodoroDisplay() {
|
| 586 |
-
const minutes = Math.floor(state.pomodoro.timeRemaining / 60);
|
| 587 |
-
const seconds = state.pomodoro.timeRemaining % 60;
|
| 588 |
-
document.getElementById('pomodoro-timer').textContent =
|
| 589 |
-
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
| 590 |
-
|
| 591 |
-
const statusEl = document.getElementById('pomodoro-status');
|
| 592 |
-
statusEl.textContent = state.pomodoro.isBreak ? 'Pausenzeit läuft...' : 'Arbeitszeit läuft...';
|
| 593 |
-
}
|
| 594 |
-
|
| 595 |
-
function handlePomodoroComplete() {
|
| 596 |
-
clearInterval(state.pomodoro.interval);
|
| 597 |
-
|
| 598 |
-
if (!state.pomodoro.isBreak) {
|
| 599 |
-
// Work session complete
|
| 600 |
-
const elapsed = state.pomodoro.totalTime;
|
| 601 |
-
state.focusTask.spentTime += elapsed;
|
| 602 |
-
saveToLocalStorage();
|
| 603 |
-
|
| 604 |
-
// Start break
|
| 605 |
-
const breakTime = parseInt(localStorage.getItem('pomodoroBreakTime')) || 5;
|
| 606 |
-
startPomodoro(breakTime * 60, true);
|
| 607 |
-
|
| 608 |
-
// Notification
|
| 609 |
-
if ('Notification' in window && Notification.permission === 'granted') {
|
| 610 |
-
new Notification('Pause!', { body: 'Arbeitszeit abgeschlossen. Zeit für eine Pause!' });
|
| 611 |
-
}
|
| 612 |
-
} else {
|
| 613 |
-
// Break complete
|
| 614 |
-
if ('Notification' in window && Notification.permission === 'granted') {
|
| 615 |
-
new Notification('Weiterarbeiten!', { body: 'Pausenzeit ist vorbei.' });
|
| 616 |
-
}
|
| 617 |
-
}
|
| 618 |
-
}
|
| 619 |
-
|
| 620 |
-
function pausePomodoro() {
|
| 621 |
-
state.pomodoro.isRunning = false;
|
| 622 |
-
clearInterval(state.pomodoro.interval);
|
| 623 |
-
document.getElementById('focus-pause').classList.add('hidden');
|
| 624 |
-
document.getElementById('focus-play').classList.remove('hidden');
|
| 625 |
-
}
|
| 626 |
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
function savePomodoroSettings() {
|
| 648 |
-
localStorage.setItem('pomodoroWorkTime', document.getElementById('pomodoro-work').value);
|
| 649 |
-
localStorage.setItem('pomodoroBreakTime', document.getElementById('pomodoro-break').value);
|
| 650 |
-
}
|
| 651 |
-
// Timeline Functions
|
| 652 |
-
function initializeTimeline() {
|
| 653 |
-
// Setup drag start for task items
|
| 654 |
-
document.addEventListener('dragstart', (e) => {
|
| 655 |
-
if (e.target.tagName === 'TASK-ITEM') {
|
| 656 |
-
const taskId = e.target.getAttribute('task-id');
|
| 657 |
-
e.dataTransfer.setData('text/plain', taskId);
|
| 658 |
-
}
|
| 659 |
-
});
|
| 660 |
-
}
|
| 661 |
-
function handleDragStart(e) {
|
| 662 |
-
if (e.target.classList.contains('task-item')) {
|
| 663 |
-
const taskId = e.target.getAttribute('task-id');
|
| 664 |
-
e.dataTransfer.setData('text/plain', taskId);
|
| 665 |
-
e.target.classList.add('dragging');
|
| 666 |
-
}
|
| 667 |
-
}
|
| 668 |
-
|
| 669 |
-
function handleDragOver(e) {
|
| 670 |
-
e.preventDefault();
|
| 671 |
-
|
| 672 |
-
if (e.target.classList.contains('timeline-hour')) {
|
| 673 |
-
e.target.classList.add('drag-over');
|
| 674 |
-
}
|
| 675 |
-
}
|
| 676 |
-
function handleDragEnd(e) {
|
| 677 |
-
e.target.classList.remove('dragging');
|
| 678 |
-
document.querySelectorAll('.drag-over').forEach(el => {
|
| 679 |
-
el.classList.remove('drag-over');
|
| 680 |
-
});
|
| 681 |
-
}
|
| 682 |
-
function scheduleTask(taskId, hour, minute) {
|
| 683 |
-
const taskDate = new Date();
|
| 684 |
-
taskDate.setHours(0, 0, 0, 0); // Set to start of day for consistent date matching
|
| 685 |
-
|
| 686 |
-
const startTime = new Date();
|
| 687 |
-
startTime.setHours(hour, minute, 0, 0);
|
| 688 |
-
|
| 689 |
-
// Remove existing schedule for this task today
|
| 690 |
-
state.timelineTasks = state.timelineTasks.filter(t => {
|
| 691 |
-
const scheduleDate = new Date(t.date);
|
| 692 |
-
scheduleDate.setHours(0, 0, 0, 0);
|
| 693 |
-
return !(scheduleDate.getTime() === taskDate.getTime() && t.taskId === taskId);
|
| 694 |
-
});
|
| 695 |
-
|
| 696 |
-
// Add new schedule
|
| 697 |
-
state.timelineTasks.push({
|
| 698 |
-
taskId,
|
| 699 |
-
date: taskDate.toISOString(),
|
| 700 |
-
startTime: startTime.toISOString()
|
| 701 |
-
});
|
| 702 |
-
|
| 703 |
-
saveToLocalStorage();
|
| 704 |
-
|
| 705 |
-
// Update timeline sidebar
|
| 706 |
-
const timelineSidebar = document.getElementById('timeline-sidebar');
|
| 707 |
-
if (timelineSidebar && timelineSidebar.updateTimeline) {
|
| 708 |
-
timelineSidebar.updateTimeline();
|
| 709 |
-
}
|
| 710 |
-
}
|
| 711 |
-
// Utilities
|
| 712 |
-
function formatTime(minutes) {
|
| 713 |
-
const h = Math.floor(minutes / 60);
|
| 714 |
-
const m = minutes % 60;
|
| 715 |
-
return `${h}h${m > 0 ? ` ${m}m` : ''}`;
|
| 716 |
-
}
|
| 717 |
-
|
| 718 |
-
// Request notification permission
|
| 719 |
-
if ('Notification' in window && Notification.permission === 'default') {
|
| 720 |
-
Notification.requestPermission();
|
| 721 |
-
}
|
| 722 |
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
container.appendChild(draggable);
|
| 748 |
-
} else {
|
| 749 |
-
container.insertBefore(draggable, afterElement);
|
| 750 |
-
}
|
| 751 |
-
});
|
| 752 |
-
});
|
| 753 |
-
}
|
| 754 |
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
const task = state.tasks.find(t => t.id === taskId);
|
| 772 |
-
if (!task) return;
|
| 773 |
-
|
| 774 |
-
const modal = document.createElement('div');
|
| 775 |
-
modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
|
| 776 |
-
modal.innerHTML = `
|
| 777 |
<div class="bg-gray-800 rounded-xl p-6 max-w-md w-full">
|
| 778 |
-
<h3 class="text-xl font-semibold mb-4">Aufgabe
|
| 779 |
-
<form id="
|
| 780 |
<div>
|
| 781 |
<label class="block text-sm font-medium mb-2">Titel</label>
|
| 782 |
-
<input id="
|
| 783 |
class="w-full bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 784 |
</div>
|
| 785 |
<div>
|
| 786 |
<label class="block text-sm font-medium mb-2">Geschätzte Zeit</label>
|
| 787 |
<div class="flex space-x-2">
|
| 788 |
-
<input id="
|
| 789 |
class="w-full bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 790 |
-
<input id="
|
| 791 |
class="w-full bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 792 |
</div>
|
| 793 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 794 |
<div class="flex justify-end space-x-2">
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
</button>
|
| 802 |
-
</div>
|
| 803 |
-
</form>
|
| 804 |
</div>
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
const form = modal.querySelector('#edit-task-form');
|
| 810 |
-
form.addEventListener('submit', (e) => {
|
| 811 |
-
e.preventDefault();
|
| 812 |
-
handleEditTask(taskId);
|
| 813 |
-
modal.remove();
|
| 814 |
-
});
|
| 815 |
-
}
|
| 816 |
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="de" class="dark">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TaskForge Pro</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 10 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 11 |
+
</head>
|
| 12 |
+
<body class="bg-gray-900 text-gray-100 min-h-screen">
|
| 13 |
+
<!-- Header with Navigation -->
|
| 14 |
+
<header class="bg-gray-800 border-b border-gray-700 sticky top-0 z-40">
|
| 15 |
+
<div class="container mx-auto px-4 py-4">
|
| 16 |
+
<div class="flex items-center justify-between">
|
| 17 |
+
<h1 class="text-2xl font-bold text-orange-500">TaskForge Pro</h1>
|
| 18 |
+
<nav class="flex items-center space-x-4">
|
| 19 |
+
<button id="view-home" class="nav-btn active" data-view="home">
|
| 20 |
+
<i data-feather="home"></i> Übersicht
|
| 21 |
+
</button>
|
| 22 |
+
<button id="view-timeline" class="nav-btn" data-view="timeline">
|
| 23 |
+
<i data-feather="clock"></i> Timeline
|
| 24 |
+
</button>
|
| 25 |
+
<button id="view-settings" class="nav-btn" data-view="settings">
|
| 26 |
+
<i data-feather="settings"></i> Einstellungen
|
| 27 |
+
</button>
|
| 28 |
+
</nav>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
</header>
|
| 32 |
+
|
| 33 |
+
<!-- Main Content -->
|
| 34 |
+
<main class="container mx-auto px-4 py-6">
|
| 35 |
+
<!-- Home View -->
|
| 36 |
+
<div id="home-view" class="view active">
|
| 37 |
+
<!-- Tag Progress Bars -->
|
| 38 |
+
<div id="tag-progress-container" class="mb-6 space-y-3"></div>
|
| 39 |
+
|
| 40 |
+
<!-- Main Progress Bar -->
|
| 41 |
+
<div class="bg-gray-800 rounded-xl p-6 mb-8 shadow-lg">
|
| 42 |
+
<div class="flex items-center justify-between mb-4">
|
| 43 |
+
<h2 class="text-xl font-semibold">Gesamtfortschritt</h2>
|
| 44 |
+
<span id="total-progress-text" class="text-sm text-gray-400">0% (0h / 0h)</span>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="w-full bg-gray-700 rounded-full h-4 overflow-hidden">
|
| 47 |
+
<div id="total-progress-bar" class="bg-gradient-to-r from-orange-500 to-orange-600 h-full rounded-full transition-all duration-500" style="width: 0%"></div>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
|
| 51 |
+
<!-- Task Lists -->
|
| 52 |
+
<div class="grid md:grid-cols-2 gap-6">
|
| 53 |
+
<!-- Open Tasks -->
|
| 54 |
+
<div class="bg-gray-800 rounded-xl shadow-lg">
|
| 55 |
+
<div class="p-4 border-b border-gray-700">
|
| 56 |
+
<h3 class="text-lg font-semibold flex items-center">
|
| 57 |
+
<i data-feather="circle" class="text-orange-500 mr-2"></i>
|
| 58 |
+
Offene Aufgaben (<span id="open-count">0</span>)
|
| 59 |
+
</h3>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
</div>
|
| 61 |
+
<div id="open-tasks" class="p-4 space-y-3 max-h-96 overflow-y-auto"></div>
|
| 62 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
<!-- Completed Tasks -->
|
| 65 |
+
<div class="bg-gray-800 rounded-xl shadow-lg">
|
| 66 |
+
<div class="p-4 border-b border-gray-700">
|
| 67 |
+
<h3 class="text-lg font-semibold flex items-center">
|
| 68 |
+
<i data-feather="check-circle" class="text-emerald-500 mr-2"></i>
|
| 69 |
+
Erledigte Aufgaben (<span id="completed-count">0</span>)
|
| 70 |
+
</h3>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
</div>
|
| 72 |
+
<div id="completed-tasks" class="p-4 space-y-3 max-h-96 overflow-y-auto"></div>
|
| 73 |
+
</div>
|
| 74 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
<!-- Timeline View -->
|
| 78 |
+
<div id="timeline-view" class="view">
|
| 79 |
+
<div class="bg-gray-800 rounded-xl p-6 shadow-lg">
|
| 80 |
+
<div class="flex items-center justify-between mb-6">
|
| 81 |
+
<h2 class="text-xl font-semibold">Timeline - Heute</h2>
|
| 82 |
+
<div class="flex items-center space-x-4">
|
| 83 |
+
<button id="timeline-now" class="px-4 py-2 bg-orange-500 hover:bg-orange-600 rounded-lg transition">
|
| 84 |
+
Jetzt
|
| 85 |
+
</button>
|
| 86 |
+
<span id="current-time" class="text-lg font-mono"></span>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
<div id="timeline-container" class="relative">
|
| 90 |
+
<div id="timeline-tasks" class="absolute left-0 top-0 w-64 space-y-2 p-4">
|
| 91 |
+
<p class="text-sm text-gray-400">Aufgaben hierher ziehen</p>
|
| 92 |
+
</div>
|
| 93 |
+
<div id="timeline-hours" class="ml-64 border-l border-gray-600"></div>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
+
<!-- Settings View -->
|
| 99 |
+
<div id="settings-view" class="view">
|
| 100 |
+
<div class="max-w-4xl mx-auto">
|
| 101 |
+
<div class="bg-gray-800 rounded-xl p-6 shadow-lg">
|
| 102 |
+
<h2 class="text-xl font-semibold mb-6">Einstellungen</h2>
|
| 103 |
+
|
| 104 |
+
<!-- Import/Export -->
|
| 105 |
+
<div class="mb-8">
|
| 106 |
+
<h3 class="text-lg font-medium mb-4">Import/Export</h3>
|
| 107 |
+
<div class="space-y-4">
|
| 108 |
+
<div>
|
| 109 |
+
<label class="block text-sm font-medium mb-2">Markdown importieren</label>
|
| 110 |
+
<textarea id="import-md" class="w-full h-64 bg-gray-700 rounded-lg p-4 text-gray-100"
|
| 111 |
+
placeholder="- [ ] Aufgabe | 2h | #tag1 #tag2 - [x] Erledigte Aufgabe | 1h 30m | #tag1"></textarea>
|
| 112 |
+
<button id="import-btn" class="mt-2 px-4 py-2 bg-emerald-500 hover:bg-emerald-600 rounded-lg transition">
|
| 113 |
+
Importieren
|
| 114 |
+
</button>
|
| 115 |
+
</div>
|
| 116 |
+
<div>
|
| 117 |
+
<button id="export-btn" class="px-4 py-2 bg-orange-500 hover:bg-orange-600 rounded-lg transition">
|
| 118 |
+
Als Markdown exportieren
|
| 119 |
+
</button>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
|
| 124 |
+
<!-- Tags -->
|
| 125 |
+
<div class="mb-8">
|
| 126 |
+
<h3 class="text-lg font-medium mb-4">Tags verwalten</h3>
|
| 127 |
+
<div class="flex items-center space-x-2 mb-4">
|
| 128 |
+
<input id="new-tag-name" type="text" placeholder="Tag-Name"
|
| 129 |
+
class="bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 130 |
+
<input id="new-tag-color" type="color" value="#f97316"
|
| 131 |
+
class="w-16 h-10 bg-gray-700 rounded cursor-pointer">
|
| 132 |
+
<button id="add-tag-btn" class="px-4 py-2 bg-emerald-500 hover:bg-emerald-600 rounded-lg transition">
|
| 133 |
+
<i data-feather="plus"></i>
|
| 134 |
+
</button>
|
| 135 |
+
</div>
|
| 136 |
+
<div id="tags-list" class="flex flex-wrap gap-2"></div>
|
| 137 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
+
<!-- Pomodoro Settings -->
|
| 140 |
+
<div>
|
| 141 |
+
<h3 class="text-lg font-medium mb-4">Pomodoro</h3>
|
| 142 |
+
<div class="grid grid-cols-2 gap-4">
|
| 143 |
+
<div>
|
| 144 |
+
<label class="block text-sm font-medium mb-2">Arbeitszeit (Min.)</label>
|
| 145 |
+
<input id="pomodoro-work" type="number" value="25" min="1" max="60"
|
| 146 |
+
class="w-full bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 147 |
+
</div>
|
| 148 |
+
<div>
|
| 149 |
+
<label class="block text-sm font-medium mb-2">Pause (Min.)</label>
|
| 150 |
+
<input id="pomodoro-break" type="number" value="5" min="1" max="30"
|
| 151 |
+
class="w-full bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
</main>
|
| 159 |
|
| 160 |
+
<!-- Add Task Modal -->
|
| 161 |
+
<div id="add-task-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
<div class="bg-gray-800 rounded-xl p-6 max-w-md w-full">
|
| 163 |
+
<h3 class="text-xl font-semibold mb-4">Aufgabe hinzufügen</h3>
|
| 164 |
+
<form id="add-task-form" class="space-y-4">
|
| 165 |
<div>
|
| 166 |
<label class="block text-sm font-medium mb-2">Titel</label>
|
| 167 |
+
<input id="task-title" type="text" required
|
| 168 |
class="w-full bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 169 |
</div>
|
| 170 |
<div>
|
| 171 |
<label class="block text-sm font-medium mb-2">Geschätzte Zeit</label>
|
| 172 |
<div class="flex space-x-2">
|
| 173 |
+
<input id="task-hours" type="number" placeholder="h" min="0" max="24" value="0"
|
| 174 |
class="w-full bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 175 |
+
<input id="task-minutes" type="number" placeholder="m" min="0" max="59" step="15" value="30"
|
| 176 |
class="w-full bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 177 |
</div>
|
| 178 |
</div>
|
| 179 |
+
<div>
|
| 180 |
+
<label class="block text-sm font-medium mb-2">Tags</label>
|
| 181 |
+
<div id="modal-tags-list" class="flex flex-wrap gap-2 mb-2"></div>
|
| 182 |
+
</div>
|
| 183 |
<div class="flex justify-end space-x-2">
|
| 184 |
+
<button type="button" id="cancel-task" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition">
|
| 185 |
+
Abbrechen
|
| 186 |
+
</button>
|
| 187 |
+
<button type="submit" class="px-4 py-2 bg-orange-500 hover:bg-orange-600 rounded-lg transition">
|
| 188 |
+
Hinzufügen
|
| 189 |
+
</button>
|
|
|
|
|
|
|
|
|
|
| 190 |
</div>
|
| 191 |
+
</form>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
+
<!-- Focus Mode -->
|
| 196 |
+
<div id="focus-mode" class="fixed inset-0 bg-gray-900 z-50 hidden flex items-center justify-center">
|
| 197 |
+
<div class="text-center max-w-2xl mx-auto p-8">
|
| 198 |
+
<h2 id="focus-task-title" class="text-3xl font-bold mb-8"></h2>
|
| 199 |
+
|
| 200 |
+
<div id="pomodoro-timer" class="text-6xl font-mono font-bold text-orange-500 mb-8"></div>
|
| 201 |
+
|
| 202 |
+
<div class="flex items-center justify-center space-x-4 mb-8">
|
| 203 |
+
<button id="focus-pause" class="p-4 bg-gray-800 hover:bg-gray-700 rounded-full transition">
|
| 204 |
+
<i data-feather="pause" class="w-8 h-8"></i>
|
| 205 |
+
</button>
|
| 206 |
+
<button id="focus-play" class="p-4 bg-gray-800 hover:bg-gray-700 rounded-full transition hidden">
|
| 207 |
+
<i data-feather="play" class="w-8 h-8"></i>
|
| 208 |
+
</button>
|
| 209 |
+
<button id="focus-stop" class="p-4 bg-red-500 hover:bg-red-600 rounded-full transition">
|
| 210 |
+
<i data-feather="square" class="w-8 h-8"></i>
|
| 211 |
+
</button>
|
| 212 |
+
</div>
|
| 213 |
+
|
| 214 |
+
<div class="text-gray-400">
|
| 215 |
+
<p id="pomodoro-status">Arbeitszeit läuft...</p>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
|
| 220 |
+
<!-- FAB Buttons -->
|
| 221 |
+
<div class="fixed bottom-6 right-6 flex flex-col space-y-3">
|
| 222 |
+
<button id="focus-fab" class="p-4 bg-orange-500 hover:bg-orange-600 rounded-full shadow-lg transition transform hover:scale-110">
|
| 223 |
+
<i data-feather="focus"></i>
|
| 224 |
+
</button>
|
| 225 |
+
<button id="add-task-fab" class="p-4 bg-emerald-500 hover:bg-emerald-600 rounded-full shadow-lg transition transform hover:scale-110">
|
| 226 |
+
<i data-feather="plus"></i>
|
| 227 |
+
</button>
|
| 228 |
+
</div>
|
| 229 |
+
<timeline-tasks id="timeline-sidebar"></timeline-tasks>
|
| 230 |
+
<script src="components/task-item.js"></script>
|
| 231 |
+
<script src="components/tag-item.js"></script>
|
| 232 |
+
<script src="components/timeline-tasks.js"></script>
|
| 233 |
+
<script src="script.js"></script>
|
| 234 |
+
<script>feather.replace();</script>
|
| 235 |
+
<script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
|
| 236 |
+
</body>
|
| 237 |
+
</html>
|
script.js
CHANGED
|
@@ -1,241 +1,800 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
}
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
.
|
| 53 |
-
@apply transform scale-105 shadow-lg;
|
| 54 |
-
}
|
| 55 |
|
| 56 |
-
|
| 57 |
-
.
|
| 58 |
-
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
| 59 |
}
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
-
|
| 67 |
-
|
|
|
|
| 68 |
}
|
| 69 |
}
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
}
|
| 79 |
|
| 80 |
-
|
| 81 |
-
.
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
-
|
| 86 |
-
.
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
-
|
| 96 |
-
.
|
| 97 |
-
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
}
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
}
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
.
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
-
|
| 125 |
-
.nav-btn.active {
|
| 126 |
-
@apply text-orange-500 bg-gray-700;
|
| 127 |
}
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
.
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
}
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
transform: translateY(10px);
|
| 142 |
}
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
}
|
| 147 |
}
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
.
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
-
|
| 155 |
-
.
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
}
|
| 160 |
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
}
|
| 165 |
|
| 166 |
-
|
| 167 |
-
.
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
}
|
| 170 |
|
| 171 |
-
|
| 172 |
-
.
|
| 173 |
-
|
|
|
|
|
|
|
| 174 |
}
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
| 178 |
}
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
#timeline-tasks {
|
| 183 |
-
width: 100%;
|
| 184 |
-
position: static;
|
| 185 |
-
}
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
| 189 |
}
|
| 190 |
|
| 191 |
-
.
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
top: auto !important;
|
| 196 |
-
}
|
| 197 |
}
|
| 198 |
|
| 199 |
-
|
| 200 |
-
.
|
| 201 |
-
|
| 202 |
}
|
| 203 |
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
}
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
transform: translateX(100%);
|
| 216 |
}
|
| 217 |
}
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
}
|
| 223 |
-
|
| 224 |
-
.
|
| 225 |
-
|
|
|
|
|
|
|
| 226 |
}
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
}
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
}
|
| 237 |
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
}
|
|
|
|
| 1 |
+
// TaskForge Pro - Main JavaScript
|
| 2 |
+
// State management
|
| 3 |
+
window.state = {
|
| 4 |
+
tasks: [],
|
| 5 |
+
tags: [],
|
| 6 |
+
currentView: 'home',
|
| 7 |
+
focusTask: null,
|
| 8 |
+
pomodoro: {
|
| 9 |
+
isRunning: false,
|
| 10 |
+
isBreak: false,
|
| 11 |
+
timeRemaining: 0,
|
| 12 |
+
totalTime: 0,
|
| 13 |
+
interval: null
|
| 14 |
+
},
|
| 15 |
+
timelineTasks: []
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
// Initialize
|
| 19 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 20 |
+
loadFromLocalStorage();
|
| 21 |
+
initializeUI();
|
| 22 |
+
setupEventListeners();
|
| 23 |
+
render();
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
// Local Storage
|
| 27 |
+
function loadFromLocalStorage() {
|
| 28 |
+
const savedTasks = localStorage.getItem('tasks');
|
| 29 |
+
const savedTags = localStorage.getItem('tags');
|
| 30 |
+
const savedTimeline = localStorage.getItem('timeline');
|
| 31 |
+
|
| 32 |
+
if (savedTasks) {
|
| 33 |
+
state.tasks = JSON.parse(savedTasks);
|
| 34 |
+
}
|
| 35 |
+
if (savedTags) {
|
| 36 |
+
state.tags = JSON.parse(savedTags);
|
| 37 |
+
}
|
| 38 |
+
if (savedTimeline) {
|
| 39 |
+
state.timelineTasks = JSON.parse(savedTimeline);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Load Pomodoro settings
|
| 43 |
+
const workTime = localStorage.getItem('pomodoroWorkTime') || 25;
|
| 44 |
+
const breakTime = localStorage.getItem('pomodoroBreakTime') || 5;
|
| 45 |
+
document.getElementById('pomodoro-work').value = workTime;
|
| 46 |
+
document.getElementById('pomodoro-break').value = breakTime;
|
| 47 |
}
|
| 48 |
|
| 49 |
+
function saveToLocalStorage() {
|
| 50 |
+
localStorage.setItem('tasks', JSON.stringify(state.tasks));
|
| 51 |
+
localStorage.setItem('tags', JSON.stringify(state.tags));
|
| 52 |
+
localStorage.setItem('timeline', JSON.stringify(state.timelineTasks));
|
| 53 |
}
|
| 54 |
|
| 55 |
+
// UI Initialization
|
| 56 |
+
function initializeUI() {
|
| 57 |
+
// Current time display
|
| 58 |
+
updateCurrentTime();
|
| 59 |
+
setInterval(updateCurrentTime, 1000);
|
| 60 |
+
|
| 61 |
+
// Initialize timeline
|
| 62 |
+
initializeTimeline();
|
| 63 |
+
|
| 64 |
+
// Scroll to current time in timeline
|
| 65 |
+
setTimeout(scrollToCurrentTime, 100);
|
| 66 |
+
// Check for default tags
|
| 67 |
+
if (state.tags.length === 0) {
|
| 68 |
+
state.tags = [
|
| 69 |
+
{ id: '1', name: 'Wichtig', color: '#ef4444' },
|
| 70 |
+
{ id: '2', name: 'Privat', color: '#10b981' },
|
| 71 |
+
{ id: '3', name: 'Arbeit', color: '#3b82f6' }
|
| 72 |
+
];
|
| 73 |
+
saveToLocalStorage();
|
| 74 |
+
}
|
| 75 |
}
|
| 76 |
|
| 77 |
+
function updateCurrentTime() {
|
| 78 |
+
const now = new Date();
|
| 79 |
+
const timeString = now.toLocaleTimeString('de-DE', {
|
| 80 |
+
hour: '2-digit',
|
| 81 |
+
minute: '2-digit'
|
| 82 |
+
});
|
| 83 |
+
document.getElementById('current-time').textContent = timeString;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function setupEventListeners() {
|
| 87 |
+
// Navigation
|
| 88 |
+
document.querySelectorAll('.nav-btn').forEach(btn => {
|
| 89 |
+
btn.addEventListener('click', (e) => {
|
| 90 |
+
const view = e.currentTarget.dataset.view;
|
| 91 |
+
switchView(view);
|
| 92 |
+
});
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
+
// FAB buttons
|
| 96 |
+
document.getElementById('add-task-fab').addEventListener('click', openAddTaskModal);
|
| 97 |
+
document.getElementById('focus-fab').addEventListener('click', enterFocusMode);
|
| 98 |
+
|
| 99 |
+
// Add task modal
|
| 100 |
+
document.getElementById('add-task-form').addEventListener('submit', handleAddTask);
|
| 101 |
+
document.getElementById('cancel-task').addEventListener('click', closeAddTaskModal);
|
| 102 |
+
|
| 103 |
+
// Settings
|
| 104 |
+
document.getElementById('import-btn').addEventListener('click', handleImport);
|
| 105 |
+
document.getElementById('export-btn').addEventListener('click', handleExport);
|
| 106 |
+
document.getElementById('add-tag-btn').addEventListener('click', handleAddTag);
|
| 107 |
+
|
| 108 |
+
document.getElementById('pomodoro-work').addEventListener('change', savePomodoroSettings);
|
| 109 |
+
document.getElementById('pomodoro-break').addEventListener('change', savePomodoroSettings);
|
| 110 |
|
| 111 |
+
// Focus mode
|
| 112 |
+
document.getElementById('focus-pause').addEventListener('click', pausePomodoro);
|
| 113 |
+
document.getElementById('focus-play').addEventListener('click', resumePomodoro);
|
| 114 |
+
document.getElementById('focus-stop').addEventListener('click', stopPomodoro);
|
|
|
|
|
|
|
| 115 |
|
| 116 |
+
// Timeline
|
| 117 |
+
document.getElementById('timeline-now').addEventListener('click', scrollToCurrentTime);
|
|
|
|
| 118 |
}
|
| 119 |
|
| 120 |
+
// View Management
|
| 121 |
+
function switchView(view) {
|
| 122 |
+
state.currentView = view;
|
| 123 |
+
|
| 124 |
+
// Update nav buttons
|
| 125 |
+
document.querySelectorAll('.nav-btn').forEach(btn => {
|
| 126 |
+
btn.classList.toggle('active', btn.dataset.view === view);
|
| 127 |
+
});
|
| 128 |
+
|
| 129 |
+
// Hide all views first
|
| 130 |
+
document.querySelectorAll('.view').forEach(v => {
|
| 131 |
+
v.classList.remove('active');
|
| 132 |
+
v.style.display = 'none';
|
| 133 |
+
});
|
| 134 |
+
|
| 135 |
+
// Show only the active view
|
| 136 |
+
const activeView = document.getElementById(`${view}-view`);
|
| 137 |
+
if (activeView) {
|
| 138 |
+
activeView.style.display = 'block';
|
| 139 |
+
activeView.classList.add('active');
|
| 140 |
}
|
| 141 |
+
|
| 142 |
+
if (view === 'timeline') {
|
| 143 |
+
renderTimeline();
|
| 144 |
}
|
| 145 |
}
|
| 146 |
+
// Task Management
|
| 147 |
+
function addTask(task) {
|
| 148 |
+
const newTask = {
|
| 149 |
+
id: Date.now().toString(),
|
| 150 |
+
title: task.title,
|
| 151 |
+
completed: false,
|
| 152 |
+
estimatedTime: task.estimatedTime, // in minutes
|
| 153 |
+
spentTime: 0, // in minutes
|
| 154 |
+
tags: task.tags || [],
|
| 155 |
+
createdAt: new Date().toISOString()
|
| 156 |
+
};
|
| 157 |
+
|
| 158 |
+
state.tasks.push(newTask);
|
| 159 |
+
saveToLocalStorage();
|
| 160 |
+
render();
|
| 161 |
+
|
| 162 |
+
return newTask;
|
| 163 |
}
|
| 164 |
|
| 165 |
+
function updateTask(taskId, updates) {
|
| 166 |
+
const task = state.tasks.find(t => t.id === taskId);
|
| 167 |
+
if (task) {
|
| 168 |
+
Object.assign(task, updates);
|
| 169 |
+
saveToLocalStorage();
|
| 170 |
+
render();
|
| 171 |
+
}
|
| 172 |
}
|
| 173 |
|
| 174 |
+
function deleteTask(taskId) {
|
| 175 |
+
state.tasks = state.tasks.filter(t => t.id !== taskId);
|
| 176 |
+
state.timelineTasks = state.timelineTasks.filter(t => t.taskId !== taskId);
|
| 177 |
+
saveToLocalStorage();
|
| 178 |
+
render();
|
| 179 |
+
}
|
| 180 |
+
function toggleTaskComplete(taskId) {
|
| 181 |
+
const task = state.tasks.find(t => t.id === taskId);
|
| 182 |
+
if (task) {
|
| 183 |
+
task.completed = !task.completed;
|
| 184 |
+
saveToLocalStorage();
|
| 185 |
+
|
| 186 |
+
// Animate task completion
|
| 187 |
+
const taskElement = document.querySelector(`task-item[task-id="${taskId}"]`);
|
| 188 |
+
if (taskElement) {
|
| 189 |
+
taskElement.classList.add('completed');
|
| 190 |
+
setTimeout(() => {
|
| 191 |
+
render();
|
| 192 |
+
}, 600); // Match animation duration
|
| 193 |
+
} else {
|
| 194 |
+
render();
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
}
|
| 198 |
+
// Modal Functions
|
| 199 |
+
function openAddTaskModal() {
|
| 200 |
+
const modal = document.getElementById('add-task-modal');
|
| 201 |
+
modal.classList.remove('hidden');
|
| 202 |
+
renderModalTags();
|
| 203 |
+
document.getElementById('task-title').focus();
|
| 204 |
}
|
| 205 |
+
function closeAddTaskModal() {
|
| 206 |
+
const modal = document.getElementById('add-task-modal');
|
| 207 |
+
modal.classList.add('hidden');
|
| 208 |
+
document.getElementById('add-task-form').reset();
|
| 209 |
}
|
| 210 |
|
| 211 |
+
function handleAddTask(e) {
|
| 212 |
+
e.preventDefault();
|
| 213 |
+
|
| 214 |
+
const title = document.getElementById('task-title').value;
|
| 215 |
+
const hours = parseInt(document.getElementById('task-hours').value) || 0;
|
| 216 |
+
const minutes = parseInt(document.getElementById('task-minutes').value) || 0;
|
| 217 |
+
const estimatedTime = hours * 60 + minutes;
|
| 218 |
+
|
| 219 |
+
const selectedTags = Array.from(document.querySelectorAll('#modal-tags-list input:checked')).map(cb => cb.value);
|
| 220 |
+
|
| 221 |
+
addTask({
|
| 222 |
+
title,
|
| 223 |
+
estimatedTime,
|
| 224 |
+
tags: selectedTags
|
| 225 |
+
});
|
| 226 |
+
|
| 227 |
+
closeAddTaskModal();
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
function renderModalTags() {
|
| 231 |
+
const container = document.getElementById('modal-tags-list');
|
| 232 |
+
container.innerHTML = state.tags.map(tag => `
|
| 233 |
+
<label class="flex items-center space-x-2 cursor-pointer">
|
| 234 |
+
<input type="checkbox" value="${tag.id}" class="custom-checkbox">
|
| 235 |
+
<span class="tag-badge" style="background-color: ${tag.color}20; color: ${tag.color}">
|
| 236 |
+
${tag.name}
|
| 237 |
+
</span>
|
| 238 |
+
</label>
|
| 239 |
+
`).join('');
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// Tag Management
|
| 243 |
+
function addTag(tag) {
|
| 244 |
+
state.tags.push({
|
| 245 |
+
id: Date.now().toString(),
|
| 246 |
+
name: tag.name,
|
| 247 |
+
color: tag.color
|
| 248 |
+
});
|
| 249 |
+
saveToLocalStorage();
|
| 250 |
+
render();
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
function deleteTag(tagId) {
|
| 254 |
+
state.tags = state.tags.filter(t => t.id !== tagId);
|
| 255 |
+
// Remove tag from all tasks
|
| 256 |
+
state.tasks.forEach(task => {
|
| 257 |
+
task.tags = task.tags.filter(t => t !== tagId);
|
| 258 |
+
});
|
| 259 |
+
saveToLocalStorage();
|
| 260 |
+
render();
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
function handleAddTag() {
|
| 264 |
+
const name = document.getElementById('new-tag-name').value;
|
| 265 |
+
const color = document.getElementById('new-tag-color').value;
|
| 266 |
+
|
| 267 |
+
if (name) {
|
| 268 |
+
addTag({ name, color });
|
| 269 |
+
document.getElementById('new-tag-name').value = '';
|
| 270 |
+
}
|
| 271 |
}
|
| 272 |
|
| 273 |
+
// Import/Export
|
| 274 |
+
function handleImport() {
|
| 275 |
+
const markdown = document.getElementById('import-md').value;
|
| 276 |
+
if (!markdown) return;
|
| 277 |
+
|
| 278 |
+
const tasks = parseMarkdown(markdown);
|
| 279 |
+
tasks.forEach(task => {
|
| 280 |
+
// Check if task already exists
|
| 281 |
+
const exists = state.tasks.some(t => t.title === task.title);
|
| 282 |
+
if (!exists) {
|
| 283 |
+
state.tasks.push({
|
| 284 |
+
id: Date.now().toString() + Math.random(),
|
| 285 |
+
title: task.title,
|
| 286 |
+
completed: task.completed,
|
| 287 |
+
estimatedTime: task.estimatedTime,
|
| 288 |
+
spentTime: task.spentTime || 0,
|
| 289 |
+
tags: [],
|
| 290 |
+
createdAt: new Date().toISOString()
|
| 291 |
+
});
|
| 292 |
+
}
|
| 293 |
+
});
|
| 294 |
+
|
| 295 |
+
saveToLocalStorage();
|
| 296 |
+
render();
|
| 297 |
+
document.getElementById('import-md').value = '';
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
function handleExport() {
|
| 301 |
+
const markdown = generateMarkdown();
|
| 302 |
+
const blob = new Blob([markdown], { type: 'text/markdown' });
|
| 303 |
+
const url = URL.createObjectURL(blob);
|
| 304 |
+
const a = document.createElement('a');
|
| 305 |
+
a.href = url;
|
| 306 |
+
a.download = 'tasks.md';
|
| 307 |
+
a.click();
|
| 308 |
+
URL.revokeObjectURL(url);
|
| 309 |
+
}
|
| 310 |
+
function parseMarkdown(markdown) {
|
| 311 |
+
const lines = markdown.split('\n');
|
| 312 |
+
const tasks = [];
|
| 313 |
+
|
| 314 |
+
const taskRegex = /^\s*-\s*\[([ x])\]\s+(.+?)(?:\s*\|\s*(.+?)(?:\s*\|\s*(.+?))?)?\s*$/;
|
| 315 |
+
|
| 316 |
+
lines.forEach(line => {
|
| 317 |
+
const match = line.match(taskRegex);
|
| 318 |
+
if (match) {
|
| 319 |
+
const completed = match[1] === 'x';
|
| 320 |
+
const title = match[2].trim();
|
| 321 |
+
const timePart = match[3] ? match[3].trim() : '0h';
|
| 322 |
+
const tagsPart = match[4] ? match[4].trim() : '';
|
| 323 |
+
|
| 324 |
+
// Parse time
|
| 325 |
+
const timeMatch = timePart.match(/(\d+)(?:\s*h\s*)?(?:(?:\s*(\d+)\s*m\s*)?)?/);
|
| 326 |
+
const hours = parseInt(timeMatch[1]) || 0;
|
| 327 |
+
const minutes = parseInt(timeMatch[2]) || 0;
|
| 328 |
+
const estimatedTime = hours * 60 + minutes;
|
| 329 |
+
|
| 330 |
+
tasks.push({
|
| 331 |
+
title,
|
| 332 |
+
completed,
|
| 333 |
+
estimatedTime
|
| 334 |
+
});
|
| 335 |
+
}
|
| 336 |
+
});
|
| 337 |
+
return tasks;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
function generateMarkdown() {
|
| 341 |
+
return state.tasks.map(task => {
|
| 342 |
+
const status = task.completed ? 'x' : ' ';
|
| 343 |
+
const hours = Math.floor(task.estimatedTime / 60);
|
| 344 |
+
const minutes = task.estimatedTime % 60;
|
| 345 |
+
const timeStr = `${hours}h${minutes > 0 ? ` ${minutes}m` : ''}`;
|
| 346 |
+
return `- [${status}] ${task.title} | ${timeStr}`;
|
| 347 |
+
}).join('\n');
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
// Rendering
|
| 351 |
+
function render() {
|
| 352 |
+
renderTasks();
|
| 353 |
+
renderProgressBars();
|
| 354 |
+
renderTags();
|
| 355 |
+
renderTimeline();
|
| 356 |
+
}
|
| 357 |
+
function renderTasks() {
|
| 358 |
+
const openContainer = document.getElementById('open-tasks');
|
| 359 |
+
const completedContainer = document.getElementById('completed-tasks');
|
| 360 |
+
|
| 361 |
+
const openTasks = state.tasks.filter(t => !t.completed);
|
| 362 |
+
const completedTasks = state.tasks.filter(t => t.completed);
|
| 363 |
+
|
| 364 |
+
// Render open tasks
|
| 365 |
+
if (openTasks.length === 0) {
|
| 366 |
+
openContainer.innerHTML = '<div class="empty-state"><i data-feather="inbox"></i><p>Keine offenen Aufgaben</p></div>';
|
| 367 |
+
} else {
|
| 368 |
+
openContainer.innerHTML = '';
|
| 369 |
+
openTasks.forEach(task => {
|
| 370 |
+
const taskElement = document.createElement('task-item');
|
| 371 |
+
taskElement.setAttribute('task-id', task.id);
|
| 372 |
+
taskElement.setAttribute('completed', 'false');
|
| 373 |
+
openContainer.appendChild(taskElement);
|
| 374 |
+
});
|
| 375 |
}
|
| 376 |
+
|
| 377 |
+
// Render completed tasks
|
| 378 |
+
if (completedTasks.length === 0) {
|
| 379 |
+
completedContainer.innerHTML = '<div class="empty-state"><i data-feather="check-circle"></i><p>Keine erledigten Aufgaben</p></div>';
|
| 380 |
+
} else {
|
| 381 |
+
completedContainer.innerHTML = '';
|
| 382 |
+
completedTasks.forEach(task => {
|
| 383 |
+
const taskElement = document.createElement('task-item');
|
| 384 |
+
taskElement.setAttribute('task-id', task.id);
|
| 385 |
+
taskElement.setAttribute('completed', 'true');
|
| 386 |
+
completedContainer.appendChild(taskElement);
|
| 387 |
+
});
|
| 388 |
}
|
| 389 |
+
|
| 390 |
+
document.getElementById('open-count').textContent = openTasks.length;
|
| 391 |
+
document.getElementById('completed-count').textContent = completedTasks.length;
|
| 392 |
+
|
| 393 |
+
// Replace feather icons
|
| 394 |
+
feather.replace();
|
| 395 |
+
}
|
| 396 |
+
function renderProgressBars() {
|
| 397 |
+
// Overall progress
|
| 398 |
+
const totalEstimated = state.tasks.reduce((sum, t) => sum + t.estimatedTime, 0) || 1;
|
| 399 |
+
const totalSpent = state.tasks.reduce((sum, t) => sum + t.spentTime, 0);
|
| 400 |
+
const totalProgress = (totalSpent / totalEstimated) * 100;
|
| 401 |
+
|
| 402 |
+
document.getElementById('total-progress-bar').style.width = `${totalProgress}%`;
|
| 403 |
+
document.getElementById('total-progress-text').textContent =
|
| 404 |
+
`${Math.round(totalProgress)}% (${formatTime(totalSpent)} / ${formatTime(totalEstimated)})`;
|
| 405 |
+
|
| 406 |
+
// Tag progress bars - only for tags with tasks
|
| 407 |
+
const container = document.getElementById('tag-progress-container');
|
| 408 |
+
container.innerHTML = state.tags
|
| 409 |
+
.filter(tag => state.tasks.some(t => t.tags.includes(tag.id))) // Only tags with tasks
|
| 410 |
+
.map(tag => {
|
| 411 |
+
const tagTasks = state.tasks.filter(t => t.tags.includes(tag.id));
|
| 412 |
+
const tagEstimated = tagTasks.reduce((sum, t) => sum + t.estimatedTime, 0) || 1;
|
| 413 |
+
const tagSpent = tagTasks.reduce((sum, t) => sum + t.spentTime, 0);
|
| 414 |
+
const tagProgress = (tagSpent / tagEstimated) * 100;
|
| 415 |
+
|
| 416 |
+
return `
|
| 417 |
+
<div class="bg-gray-800 rounded-lg p-4 shadow transition-all hover:scale-[1.02]">
|
| 418 |
+
<div class="flex items-center justify-between mb-2">
|
| 419 |
+
<span class="font-medium" style="color: ${tag.color}">${tag.name}</span>
|
| 420 |
+
<span class="text-sm text-gray-400">
|
| 421 |
+
${Math.round(tagProgress)}% (${formatTime(tagSpent)} / ${formatTime(tagEstimated)})
|
| 422 |
+
</span>
|
| 423 |
+
</div>
|
| 424 |
+
<div class="w-full bg-gray-700 rounded-full h-2 overflow-hidden">
|
| 425 |
+
<div class="h-full rounded-full transition-all duration-500"
|
| 426 |
+
style="width: ${tagProgress}%; background-color: ${tag.color}"></div>
|
| 427 |
+
</div>
|
| 428 |
+
</div>
|
| 429 |
+
`;
|
| 430 |
+
}).join('');
|
| 431 |
+
}
|
| 432 |
+
function renderTags() {
|
| 433 |
+
const container = document.getElementById('tags-list');
|
| 434 |
+
container.innerHTML = state.tags.map(tag => `
|
| 435 |
+
<tag-item tag-id="${tag.id}" tag-name="${tag.name}" tag-color="${tag.color}"></tag-item>
|
| 436 |
+
`).join('');
|
| 437 |
+
|
| 438 |
+
// Render tag components
|
| 439 |
+
document.querySelectorAll('tag-item').forEach(el => {
|
| 440 |
+
const tagId = el.getAttribute('tag-id');
|
| 441 |
+
el.addEventListener('delete', () => deleteTag(tagId));
|
| 442 |
+
});
|
| 443 |
+
}
|
| 444 |
+
function renderTimeline() {
|
| 445 |
+
if (state.currentView !== 'timeline') return;
|
| 446 |
+
|
| 447 |
+
const container = document.getElementById('timeline-hours');
|
| 448 |
+
container.innerHTML = '';
|
| 449 |
+
|
| 450 |
+
// Generate hours
|
| 451 |
+
for (let h = 6; h <= 22; h++) {
|
| 452 |
+
const hourDiv = document.createElement('div');
|
| 453 |
+
hourDiv.className = 'timeline-hour relative border-b border-gray-700';
|
| 454 |
+
hourDiv.dataset.hour = h;
|
| 455 |
+
hourDiv.style.minHeight = '60px';
|
| 456 |
+
|
| 457 |
+
const timeLabel = document.createElement('span');
|
| 458 |
+
timeLabel.className = 'absolute -ml-20 mt-2 text-sm text-gray-400 w-16 text-right';
|
| 459 |
+
timeLabel.textContent = `${h.toString().padStart(2, '0')}:00`;
|
| 460 |
+
|
| 461 |
+
hourDiv.appendChild(timeLabel);
|
| 462 |
+
container.appendChild(hourDiv);
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
// Highlight current hour
|
| 466 |
+
highlightCurrentHour();
|
| 467 |
+
|
| 468 |
+
// Render scheduled tasks
|
| 469 |
+
renderScheduledTasks();
|
| 470 |
}
|
| 471 |
+
function highlightCurrentHour() {
|
| 472 |
+
const now = new Date();
|
| 473 |
+
const currentHour = now.getHours();
|
| 474 |
+
const currentMinute = now.getMinutes();
|
| 475 |
+
|
| 476 |
+
// Highlight current hour
|
| 477 |
+
document.querySelectorAll('.timeline-hour').forEach(el => {
|
| 478 |
+
el.classList.remove('bg-gray-750');
|
| 479 |
+
});
|
| 480 |
+
|
| 481 |
+
const hourElement = document.querySelector(`.timeline-hour[data-hour="${currentHour}"]`);
|
| 482 |
+
if (hourElement) {
|
| 483 |
+
hourElement.classList.add('bg-gray-750');
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
// Add current time indicator line
|
| 487 |
+
document.getElementById('current-time-indicator')?.remove();
|
| 488 |
+
|
| 489 |
+
const timelineContainer = document.getElementById('timeline-hours');
|
| 490 |
+
if (timelineContainer) {
|
| 491 |
+
const indicator = document.createElement('div');
|
| 492 |
+
indicator.id = 'current-time-indicator';
|
| 493 |
+
indicator.style.position = 'absolute';
|
| 494 |
+
indicator.style.left = '0';
|
| 495 |
+
indicator.style.right = '0';
|
| 496 |
+
indicator.style.height = '2px';
|
| 497 |
+
indicator.style.backgroundColor = '#ef4444';
|
| 498 |
+
indicator.style.zIndex = '10';
|
| 499 |
+
|
| 500 |
+
const hourElement = document.querySelector(`.timeline-hour[data-hour="${currentHour}"]`);
|
| 501 |
+
if (hourElement) {
|
| 502 |
+
const hourTop = hourElement.offsetTop;
|
| 503 |
+
const hourHeight = hourElement.offsetHeight;
|
| 504 |
+
const minutePosition = (currentMinute / 60) * hourHeight;
|
| 505 |
+
|
| 506 |
+
indicator.style.top = `${hourTop + minutePosition}px`;
|
| 507 |
+
timelineContainer.appendChild(indicator);
|
| 508 |
+
|
| 509 |
+
// Add time label
|
| 510 |
+
const timeLabel = document.createElement('div');
|
| 511 |
+
timeLabel.textContent = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
| 512 |
+
timeLabel.style.position = 'absolute';
|
| 513 |
+
timeLabel.style.left = '-64px';
|
| 514 |
+
timeLabel.style.top = `${hourTop + minutePosition - 10}px`;
|
| 515 |
+
timeLabel.style.backgroundColor = '#ef4444';
|
| 516 |
+
timeLabel.style.color = 'white';
|
| 517 |
+
timeLabel.style.padding = '2px 8px';
|
| 518 |
+
timeLabel.style.borderRadius = '4px';
|
| 519 |
+
timeLabel.style.fontSize = '12px';
|
| 520 |
+
timeLabel.style.zIndex = '10';
|
| 521 |
+
|
| 522 |
+
hourEl.appendChild(timeLabel);
|
| 523 |
}
|
| 524 |
+
}
|
|
|
|
|
|
|
| 525 |
}
|
| 526 |
+
function renderScheduledTasks() {
|
| 527 |
+
const today = new Date().toDateString();
|
| 528 |
+
const scheduledTasks = state.timelineTasks.filter(t =>
|
| 529 |
+
new Date(t.date).toDateString() === today
|
| 530 |
+
);
|
| 531 |
+
|
| 532 |
+
scheduledTasks.forEach(task => {
|
| 533 |
+
const taskData = state.tasks.find(t => t.id === task.taskId);
|
| 534 |
+
if (!taskData) return;
|
| 535 |
+
|
| 536 |
+
const startHour = new Date(task.startTime).getHours();
|
| 537 |
+
const startMinute = new Date(task.startTime).getMinutes();
|
| 538 |
+
const duration = taskData.estimatedTime;
|
| 539 |
+
|
| 540 |
+
const hourEl = document.querySelector(`[data-hour="${startHour}"]`);
|
| 541 |
+
if (hourEl) {
|
| 542 |
+
const taskEl = document.createElement('div');
|
| 543 |
+
taskEl.className = 'timeline-task-block';
|
| 544 |
+
taskEl.style.left = `${(startMinute / 60) * 100}%`;
|
| 545 |
+
taskEl.style.width = `${(duration / 60) * 100}%`;
|
| 546 |
+
taskEl.style.backgroundColor = task.tags[0] ?
|
| 547 |
+
state.tags.find(t => t.id === task.tags[0])?.color + '20' : '#f9731620';
|
| 548 |
+
taskEl.textContent = taskData.title;
|
| 549 |
+
taskEl.draggable = true;
|
| 550 |
+
|
| 551 |
+
hourEl.appendChild(taskEl);
|
| 552 |
+
}
|
| 553 |
+
});
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
function scrollToCurrentTime() {
|
| 557 |
+
const now = new Date();
|
| 558 |
+
const currentHour = now.getHours();
|
| 559 |
+
const hourEl = document.querySelector(`[data-hour="${currentHour}"]`);
|
| 560 |
+
|
| 561 |
+
if (hourEl) {
|
| 562 |
+
hourEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 563 |
+
}
|
| 564 |
}
|
| 565 |
|
| 566 |
+
// Focus Mode & Pomodoro
|
| 567 |
+
function enterFocusMode() {
|
| 568 |
+
// Open a task selection modal
|
| 569 |
+
const openTasks = state.tasks.filter(t => !t.completed);
|
| 570 |
+
if (openTasks.length === 0) {
|
| 571 |
+
alert('Keine offenen Aufgaben für den Fokus-Modus!');
|
| 572 |
+
return;
|
|
|
|
| 573 |
}
|
| 574 |
+
|
| 575 |
+
const modal = document.createElement('div');
|
| 576 |
+
modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
|
| 577 |
+
modal.innerHTML = `
|
| 578 |
+
<div class="bg-gray-800 rounded-xl p-6 max-w-md w-full max-h-[80vh] overflow-y-auto">
|
| 579 |
+
<h3 class="text-xl font-semibold mb-4">Aufgabe für Fokus-Modus auswählen</h3>
|
| 580 |
+
<div class="space-y-2">
|
| 581 |
+
${openTasks.map(task => `
|
| 582 |
+
<div class="bg-gray-700 p-3 rounded-lg cursor-pointer hover:bg-gray-600 transition"
|
| 583 |
+
onclick="startFocusModeWithId('${task.id}')">
|
| 584 |
+
<div class="font-medium">${task.title}</div>
|
| 585 |
+
<div class="text-sm text-gray-400">${formatTime(task.estimatedTime)}</div>
|
| 586 |
+
</div>
|
| 587 |
+
`).join('')}
|
| 588 |
+
</div>
|
| 589 |
+
<button onclick="this.closest('div[class*=\"fixed\"]').remove()"
|
| 590 |
+
class="mt-4 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition w-full">
|
| 591 |
+
Abbrechen
|
| 592 |
+
</button>
|
| 593 |
+
</div>
|
| 594 |
+
`;
|
| 595 |
+
document.body.appendChild(modal);
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
function startFocusModeWithId(taskId) {
|
| 599 |
+
const task = state.tasks.find(t => t.id === taskId);
|
| 600 |
+
if (task) {
|
| 601 |
+
document.querySelector('div[class*="fixed"]')?.remove();
|
| 602 |
+
startFocusMode(task);
|
| 603 |
}
|
| 604 |
}
|
| 605 |
+
function startFocusMode(task) {
|
| 606 |
+
state.focusTask = task;
|
| 607 |
+
document.getElementById('focus-task-title').textContent = task.title;
|
| 608 |
+
document.getElementById('focus-mode').classList.remove('hidden');
|
| 609 |
+
|
| 610 |
+
// Start pomodoro
|
| 611 |
+
const workTime = parseInt(localStorage.getItem('pomodoroWorkTime')) || 25;
|
| 612 |
+
startPomodoro(workTime * 60, false);
|
| 613 |
}
|
| 614 |
|
| 615 |
+
function startPomodoro(seconds, isBreak) {
|
| 616 |
+
state.pomodoro.isRunning = true;
|
| 617 |
+
state.pomodoro.isBreak = isBreak;
|
| 618 |
+
state.pomodoro.timeRemaining = seconds;
|
| 619 |
+
state.pomodoro.totalTime = seconds;
|
| 620 |
+
|
| 621 |
+
clearInterval(state.pomodoro.interval);
|
| 622 |
+
|
| 623 |
+
state.pomodoro.interval = setInterval(() => {
|
| 624 |
+
if (state.pomodoro.timeRemaining > 0) {
|
| 625 |
+
state.pomodoro.timeRemaining--;
|
| 626 |
+
updatePomodoroDisplay();
|
| 627 |
+
} else {
|
| 628 |
+
handlePomodoroComplete();
|
| 629 |
+
}
|
| 630 |
+
}, 1000);
|
| 631 |
+
|
| 632 |
+
updatePomodoroDisplay();
|
| 633 |
}
|
| 634 |
|
| 635 |
+
function updatePomodoroDisplay() {
|
| 636 |
+
const minutes = Math.floor(state.pomodoro.timeRemaining / 60);
|
| 637 |
+
const seconds = state.pomodoro.timeRemaining % 60;
|
| 638 |
+
document.getElementById('pomodoro-timer').textContent =
|
| 639 |
+
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
| 640 |
+
|
| 641 |
+
const statusEl = document.getElementById('pomodoro-status');
|
| 642 |
+
statusEl.textContent = state.pomodoro.isBreak ? 'Pausenzeit läuft...' : 'Arbeitszeit läuft...';
|
| 643 |
}
|
| 644 |
|
| 645 |
+
function handlePomodoroComplete() {
|
| 646 |
+
clearInterval(state.pomodoro.interval);
|
| 647 |
+
|
| 648 |
+
if (!state.pomodoro.isBreak) {
|
| 649 |
+
// Work session complete
|
| 650 |
+
const elapsed = state.pomodoro.totalTime;
|
| 651 |
+
state.focusTask.spentTime += elapsed;
|
| 652 |
+
saveToLocalStorage();
|
| 653 |
+
|
| 654 |
+
// Start break
|
| 655 |
+
const breakTime = parseInt(localStorage.getItem('pomodoroBreakTime')) || 5;
|
| 656 |
+
startPomodoro(breakTime * 60, true);
|
| 657 |
+
|
| 658 |
+
// Notification
|
| 659 |
+
if ('Notification' in window && Notification.permission === 'granted') {
|
| 660 |
+
new Notification('Pause!', { body: 'Arbeitszeit abgeschlossen. Zeit für eine Pause!' });
|
| 661 |
+
}
|
| 662 |
+
} else {
|
| 663 |
+
// Break complete
|
| 664 |
+
if ('Notification' in window && Notification.permission === 'granted') {
|
| 665 |
+
new Notification('Weiterarbeiten!', { body: 'Pausenzeit ist vorbei.' });
|
| 666 |
+
}
|
| 667 |
+
}
|
| 668 |
}
|
| 669 |
|
| 670 |
+
function pausePomodoro() {
|
| 671 |
+
state.pomodoro.isRunning = false;
|
| 672 |
+
clearInterval(state.pomodoro.interval);
|
| 673 |
+
document.getElementById('focus-pause').classList.add('hidden');
|
| 674 |
+
document.getElementById('focus-play').classList.remove('hidden');
|
| 675 |
}
|
| 676 |
|
| 677 |
+
function resumePomodoro() {
|
| 678 |
+
startPomodoro(state.pomodoro.timeRemaining, state.pomodoro.isBreak);
|
| 679 |
+
document.getElementById('focus-play').classList.add('hidden');
|
| 680 |
+
document.getElementById('focus-pause').classList.remove('hidden');
|
| 681 |
}
|
| 682 |
|
| 683 |
+
function stopPomodoro() {
|
| 684 |
+
clearInterval(state.pomodoro.interval);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
|
| 686 |
+
// Add elapsed time to task
|
| 687 |
+
if (state.focusTask && !state.pomodoro.isBreak) {
|
| 688 |
+
const elapsed = state.pomodoro.totalTime - state.pomodoro.timeRemaining;
|
| 689 |
+
state.focusTask.spentTime += elapsed;
|
| 690 |
+
saveToLocalStorage();
|
| 691 |
}
|
| 692 |
|
| 693 |
+
state.focusTask = null;
|
| 694 |
+
state.pomodoro.isRunning = false;
|
| 695 |
+
document.getElementById('focus-mode').classList.add('hidden');
|
| 696 |
+
render();
|
|
|
|
|
|
|
| 697 |
}
|
| 698 |
|
| 699 |
+
function savePomodoroSettings() {
|
| 700 |
+
localStorage.setItem('pomodoroWorkTime', document.getElementById('pomodoro-work').value);
|
| 701 |
+
localStorage.setItem('pomodoroBreakTime', document.getElementById('pomodoro-break').value);
|
| 702 |
}
|
| 703 |
|
| 704 |
+
// Timeline Functions
|
| 705 |
+
function initializeTimeline() {
|
| 706 |
+
// Initialize timeline sidebar
|
| 707 |
+
const timelineSidebar = document.createElement('timeline-sidebar');
|
| 708 |
+
document.body.appendChild(timelineSidebar);
|
| 709 |
+
|
| 710 |
+
// Listen for task drops
|
| 711 |
+
timelineSidebar.addEventListener('task-dropped', (e) => {
|
| 712 |
+
const { taskId, hour } = e.detail;
|
| 713 |
+
scheduleTask(taskId, hour, 0); // Schedule at top of hour
|
| 714 |
+
});
|
| 715 |
+
|
| 716 |
+
// Setup drag start for task items
|
| 717 |
+
document.addEventListener('dragstart', (e) => {
|
| 718 |
+
if (e.target.tagName === 'TASK-ITEM') {
|
| 719 |
+
const taskId = e.target.getAttribute('task-id');
|
| 720 |
+
e.dataTransfer.setData('text/plain', taskId);
|
| 721 |
+
}
|
| 722 |
+
});
|
| 723 |
+
}
|
| 724 |
+
function handleDragStart(e) {
|
| 725 |
+
if (e.target.classList.contains('task-item')) {
|
| 726 |
+
const taskId = e.target.getAttribute('task-id');
|
| 727 |
+
e.dataTransfer.setData('text/plain', taskId);
|
| 728 |
+
e.target.classList.add('dragging');
|
| 729 |
+
}
|
| 730 |
}
|
| 731 |
|
| 732 |
+
function handleDragOver(e) {
|
| 733 |
+
e.preventDefault();
|
| 734 |
+
|
| 735 |
+
if (e.target.classList.contains('timeline-hour')) {
|
| 736 |
+
e.target.classList.add('drag-over');
|
|
|
|
| 737 |
}
|
| 738 |
}
|
| 739 |
+
function handleDrop(e) {
|
| 740 |
+
e.preventDefault();
|
| 741 |
+
|
| 742 |
+
// Remove drag-over class from all elements
|
| 743 |
+
document.querySelectorAll('.drag-over').forEach(el => {
|
| 744 |
+
el.classList.remove('drag-over');
|
| 745 |
+
});
|
| 746 |
+
|
| 747 |
+
const taskId = e.dataTransfer.getData('text/plain');
|
| 748 |
+
const hourEl = e.target.closest('.timeline-hour');
|
| 749 |
+
|
| 750 |
+
if (hourEl && taskId) {
|
| 751 |
+
const hour = parseInt(hourEl.dataset.hour);
|
| 752 |
+
const rect = hourEl.getBoundingClientRect();
|
| 753 |
+
const x = e.clientX - rect.left;
|
| 754 |
+
const hourWidth = rect.width;
|
| 755 |
+
const minute = Math.floor((x / hourWidth) * 60);
|
| 756 |
+
|
| 757 |
+
scheduleTask(taskId, hour, minute);
|
| 758 |
+
}
|
| 759 |
}
|
| 760 |
+
function handleDragEnd(e) {
|
| 761 |
+
e.target.classList.remove('dragging');
|
| 762 |
+
document.querySelectorAll('.drag-over').forEach(el => {
|
| 763 |
+
el.classList.remove('drag-over');
|
| 764 |
+
});
|
| 765 |
}
|
| 766 |
+
function scheduleTask(taskId, hour, minute) {
|
| 767 |
+
const taskDate = new Date();
|
| 768 |
+
taskDate.setHours(0, 0, 0, 0); // Set to start of day for consistent date matching
|
| 769 |
+
|
| 770 |
+
const startTime = new Date();
|
| 771 |
+
startTime.setHours(hour, minute, 0, 0);
|
| 772 |
+
|
| 773 |
+
// Remove existing schedule for this task today
|
| 774 |
+
state.timelineTasks = state.timelineTasks.filter(t => {
|
| 775 |
+
const scheduleDate = new Date(t.date);
|
| 776 |
+
scheduleDate.setHours(0, 0, 0, 0);
|
| 777 |
+
return !(scheduleDate.getTime() === taskDate.getTime() && t.taskId === taskId);
|
| 778 |
+
});
|
| 779 |
+
|
| 780 |
+
// Add new schedule
|
| 781 |
+
state.timelineTasks.push({
|
| 782 |
+
taskId,
|
| 783 |
+
date: taskDate.toISOString(),
|
| 784 |
+
startTime: startTime.toISOString()
|
| 785 |
+
});
|
| 786 |
+
|
| 787 |
+
saveToLocalStorage();
|
| 788 |
+
renderTimeline();
|
| 789 |
}
|
| 790 |
+
// Utilities
|
| 791 |
+
function formatTime(minutes) {
|
| 792 |
+
const h = Math.floor(minutes / 60);
|
| 793 |
+
const m = minutes % 60;
|
| 794 |
+
return `${h}h${m > 0 ? ` ${m}m` : ''}`;
|
| 795 |
}
|
| 796 |
|
| 797 |
+
// Request notification permission
|
| 798 |
+
if ('Notification' in window && Notification.permission === 'default') {
|
| 799 |
+
Notification.requestPermission();
|
| 800 |
}
|
style.css
CHANGED
|
@@ -1,824 +1,241 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
tags: [],
|
| 7 |
-
currentView: 'home',
|
| 8 |
-
focusTask: null,
|
| 9 |
-
pomodoro: {
|
| 10 |
-
isRunning: false,
|
| 11 |
-
isBreak: false,
|
| 12 |
-
timeRemaining: 0,
|
| 13 |
-
totalTime: 0,
|
| 14 |
-
interval: null
|
| 15 |
-
},
|
| 16 |
-
timelineTasks: []
|
| 17 |
-
};
|
| 18 |
-
// Initialize
|
| 19 |
-
document.addEventListener('DOMContentLoaded', function() {
|
| 20 |
-
loadFromLocalStorage();
|
| 21 |
-
initializeUI();
|
| 22 |
-
setupEventListeners();
|
| 23 |
-
render();
|
| 24 |
-
});
|
| 25 |
-
// Local Storage
|
| 26 |
-
function loadFromLocalStorage() {
|
| 27 |
-
const savedTasks = localStorage.getItem('tasks');
|
| 28 |
-
const savedTags = localStorage.getItem('tags');
|
| 29 |
-
const savedTimeline = localStorage.getItem('timeline');
|
| 30 |
-
|
| 31 |
-
if (savedTasks) {
|
| 32 |
-
state.tasks = JSON.parse(savedTasks);
|
| 33 |
-
}
|
| 34 |
-
if (savedTags) {
|
| 35 |
-
state.tags = JSON.parse(savedTags);
|
| 36 |
-
}
|
| 37 |
-
if (savedTimeline) {
|
| 38 |
-
state.timelineTasks = JSON.parse(savedTimeline);
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
// Load Pomodoro settings
|
| 42 |
-
const workTime = localStorage.getItem('pomodoroWorkTime') || 25;
|
| 43 |
-
const breakTime = localStorage.getItem('pomodoroBreakTime') || 5;
|
| 44 |
-
document.getElementById('pomodoro-work').value = workTime;
|
| 45 |
-
document.getElementById('pomodoro-break').value = breakTime;
|
| 46 |
}
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
localStorage.setItem('tags', JSON.stringify(state.tags));
|
| 51 |
-
localStorage.setItem('timeline', JSON.stringify(state.timelineTasks));
|
| 52 |
}
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
// Current time display
|
| 57 |
-
updateCurrentTime();
|
| 58 |
-
setInterval(updateCurrentTime, 1000);
|
| 59 |
-
|
| 60 |
-
// Initialize timeline
|
| 61 |
-
initializeTimeline();
|
| 62 |
-
|
| 63 |
-
// Scroll to current time in timeline
|
| 64 |
-
setTimeout(scrollToCurrentTime, 100);
|
| 65 |
-
// Check for default tags
|
| 66 |
-
if (state.tags.length === 0) {
|
| 67 |
-
state.tags = [
|
| 68 |
-
{ id: '1', name: 'Wichtig', color: '#ef4444' },
|
| 69 |
-
{ id: '2', name: 'Privat', color: '#10b981' },
|
| 70 |
-
{ id: '3', name: 'Arbeit', color: '#3b82f6' }
|
| 71 |
-
];
|
| 72 |
-
saveToLocalStorage();
|
| 73 |
-
}
|
| 74 |
}
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
const timeString = now.toLocaleTimeString('de-DE', {
|
| 79 |
-
hour: '2-digit',
|
| 80 |
-
minute: '2-digit'
|
| 81 |
-
});
|
| 82 |
-
document.getElementById('current-time').textContent = timeString;
|
| 83 |
-
}
|
| 84 |
-
function setupEventListeners() {
|
| 85 |
-
// Navigation
|
| 86 |
-
document.querySelectorAll('.nav-btn').forEach(btn => {
|
| 87 |
-
btn.addEventListener('click', (e) => {
|
| 88 |
-
const view = e.currentTarget.dataset.view;
|
| 89 |
-
switchView(view);
|
| 90 |
-
});
|
| 91 |
-
});
|
| 92 |
-
|
| 93 |
-
// FAB buttons
|
| 94 |
-
document.getElementById('add-task-fab').addEventListener('click', openAddTaskModal);
|
| 95 |
-
document.getElementById('focus-fab').addEventListener('click', enterFocusMode);
|
| 96 |
-
|
| 97 |
-
// Add task modal
|
| 98 |
-
document.getElementById('add-task-form').addEventListener('submit', handleAddTask);
|
| 99 |
-
document.getElementById('cancel-task').addEventListener('click', closeAddTaskModal);
|
| 100 |
-
|
| 101 |
-
// Settings
|
| 102 |
-
document.getElementById('import-btn').addEventListener('click', handleImport);
|
| 103 |
-
document.getElementById('export-btn').addEventListener('click', handleExport);
|
| 104 |
-
document.getElementById('add-tag-btn').addEventListener('click', handleAddTag);
|
| 105 |
-
|
| 106 |
-
document.getElementById('pomodoro-work').addEventListener('change', savePomodoroSettings);
|
| 107 |
-
document.getElementById('pomodoro-break').addEventListener('change', savePomodoroSettings);
|
| 108 |
-
|
| 109 |
-
// Focus mode
|
| 110 |
-
document.getElementById('focus-pause').addEventListener('click', pausePomodoro);
|
| 111 |
-
document.getElementById('focus-play').addEventListener('click', resumePomodoro);
|
| 112 |
-
document.getElementById('focus-stop').addEventListener('click', stopPomodoro);
|
| 113 |
-
|
| 114 |
-
// Event delegation for task item events
|
| 115 |
-
document.addEventListener('delete-task', (e) => {
|
| 116 |
-
deleteTask(e.detail.taskId);
|
| 117 |
-
});
|
| 118 |
-
|
| 119 |
-
document.addEventListener('toggle-complete', (e) => {
|
| 120 |
-
toggleTaskComplete(e.detail.taskId, e.detail.completed);
|
| 121 |
-
});
|
| 122 |
-
|
| 123 |
-
document.addEventListener('update-task', (e) => {
|
| 124 |
-
updateTask(e.detail.taskId, e.detail.updates);
|
| 125 |
-
});
|
| 126 |
-
|
| 127 |
-
document.addEventListener('add-time', (e) => {
|
| 128 |
-
addTimeToTask(e.detail.taskId, e.detail.minutes);
|
| 129 |
-
});
|
| 130 |
-
|
| 131 |
-
// Timeline sidebar events
|
| 132 |
-
const timelineSidebar = document.getElementById('timeline-sidebar');
|
| 133 |
-
if (timelineSidebar) {
|
| 134 |
-
timelineSidebar.addEventListener('task-dropped', (e) => {
|
| 135 |
-
scheduleTask(e.detail.taskId, e.detail.hour, 0);
|
| 136 |
-
});
|
| 137 |
-
}
|
| 138 |
}
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
document.querySelectorAll('.nav-btn').forEach(btn => {
|
| 145 |
-
btn.classList.toggle('active', btn.dataset.view === view);
|
| 146 |
-
});
|
| 147 |
-
|
| 148 |
-
// Hide all views first
|
| 149 |
-
document.querySelectorAll('.view').forEach(v => {
|
| 150 |
-
v.classList.remove('active');
|
| 151 |
-
v.style.display = 'none';
|
| 152 |
-
});
|
| 153 |
-
|
| 154 |
-
// Show only the active view
|
| 155 |
-
const activeView = document.getElementById(`${view}-view`);
|
| 156 |
-
if (activeView) {
|
| 157 |
-
activeView.style.display = 'block';
|
| 158 |
-
activeView.classList.add('active');
|
| 159 |
-
}
|
| 160 |
}
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
id: Date.now().toString(),
|
| 165 |
-
title: task.title,
|
| 166 |
-
completed: false,
|
| 167 |
-
estimatedTime: task.estimatedTime, // in minutes
|
| 168 |
-
spentTime: 0, // in minutes
|
| 169 |
-
tags: task.tags || [],
|
| 170 |
-
createdAt: new Date().toISOString()
|
| 171 |
-
};
|
| 172 |
-
|
| 173 |
-
state.tasks.push(newTask);
|
| 174 |
-
saveToLocalStorage();
|
| 175 |
-
render();
|
| 176 |
-
|
| 177 |
-
return newTask;
|
| 178 |
}
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
saveToLocalStorage();
|
| 185 |
-
render();
|
| 186 |
-
}
|
| 187 |
}
|
| 188 |
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
}
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
saveToLocalStorage();
|
| 200 |
-
render();
|
| 201 |
-
}
|
| 202 |
}
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
modal.classList.remove('hidden');
|
| 207 |
-
renderModalTags();
|
| 208 |
-
document.getElementById('task-title').focus();
|
| 209 |
}
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
modal.classList.add('hidden');
|
| 213 |
-
document.getElementById('add-task-form').reset();
|
| 214 |
}
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
const title = document.getElementById('task-title').value;
|
| 220 |
-
const hours = parseInt(document.getElementById('task-hours').value) || 0;
|
| 221 |
-
const minutes = parseInt(document.getElementById('task-minutes').value) || 0;
|
| 222 |
-
const estimatedTime = hours * 60 + minutes;
|
| 223 |
-
|
| 224 |
-
const selectedTags = Array.from(document.querySelectorAll('#modal-tags-list input:checked')).map(cb => cb.value);
|
| 225 |
-
|
| 226 |
-
addTask({
|
| 227 |
-
title,
|
| 228 |
-
estimatedTime,
|
| 229 |
-
tags: selectedTags
|
| 230 |
-
});
|
| 231 |
-
|
| 232 |
-
closeAddTaskModal();
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
function renderModalTags() {
|
| 236 |
-
const container = document.getElementById('modal-tags-list');
|
| 237 |
-
container.innerHTML = state.tags.map(tag => `
|
| 238 |
-
<label class="flex items-center space-x-2 cursor-pointer">
|
| 239 |
-
<input type="checkbox" value="${tag.id}" class="custom-checkbox">
|
| 240 |
-
<span class="tag-badge" style="background-color: ${tag.color}20; color: ${tag.color}">
|
| 241 |
-
${tag.name}
|
| 242 |
-
</span>
|
| 243 |
-
</label>
|
| 244 |
-
`).join('');
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
// Tag Management
|
| 248 |
-
function addTag(tag) {
|
| 249 |
-
state.tags.push({
|
| 250 |
-
id: Date.now().toString(),
|
| 251 |
-
name: tag.name,
|
| 252 |
-
color: tag.color
|
| 253 |
-
});
|
| 254 |
-
saveToLocalStorage();
|
| 255 |
-
render();
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
function deleteTag(tagId) {
|
| 259 |
-
state.tags = state.tags.filter(t => t.id !== tagId);
|
| 260 |
-
// Remove tag from all tasks
|
| 261 |
-
state.tasks.forEach(task => {
|
| 262 |
-
task.tags = task.tags.filter(t => t !== tagId);
|
| 263 |
-
});
|
| 264 |
-
saveToLocalStorage();
|
| 265 |
-
render();
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
function handleAddTag() {
|
| 269 |
-
const name = document.getElementById('new-tag-name').value;
|
| 270 |
-
const color = document.getElementById('new-tag-color').value;
|
| 271 |
-
|
| 272 |
-
if (name) {
|
| 273 |
-
addTag({ name, color });
|
| 274 |
-
document.getElementById('new-tag-name').value = '';
|
| 275 |
-
}
|
| 276 |
}
|
| 277 |
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
const tasks = parseMarkdown(markdown);
|
| 284 |
-
tasks.forEach(task => {
|
| 285 |
-
// Check if task already exists
|
| 286 |
-
const exists = state.tasks.some(t => t.title === task.title);
|
| 287 |
-
if (!exists) {
|
| 288 |
-
state.tasks.push({
|
| 289 |
-
id: Date.now().toString() + Math.random(),
|
| 290 |
-
title: task.title,
|
| 291 |
-
completed: task.completed,
|
| 292 |
-
estimatedTime: task.estimatedTime,
|
| 293 |
-
spentTime: task.spentTime || 0,
|
| 294 |
-
tags: [],
|
| 295 |
-
createdAt: new Date().toISOString()
|
| 296 |
-
});
|
| 297 |
-
}
|
| 298 |
-
});
|
| 299 |
-
|
| 300 |
-
saveToLocalStorage();
|
| 301 |
-
render();
|
| 302 |
-
document.getElementById('import-md').value = '';
|
| 303 |
-
}
|
| 304 |
-
|
| 305 |
-
function handleExport() {
|
| 306 |
-
const markdown = generateMarkdown();
|
| 307 |
-
const blob = new Blob([markdown], { type: 'text/markdown' });
|
| 308 |
-
const url = URL.createObjectURL(blob);
|
| 309 |
-
const a = document.createElement('a');
|
| 310 |
-
a.href = url;
|
| 311 |
-
a.download = 'tasks.md';
|
| 312 |
-
a.click();
|
| 313 |
-
URL.revokeObjectURL(url);
|
| 314 |
-
}
|
| 315 |
-
function parseMarkdown(markdown) {
|
| 316 |
-
const lines = markdown.split('\n');
|
| 317 |
-
const tasks = [];
|
| 318 |
-
|
| 319 |
-
const taskRegex = /^\s*-\s*\[([ x])\]\s+(.+?)(?:\s*\|\s*(.+?)(?:\s*\|\s*(.+?))?)?\s*$/;
|
| 320 |
-
|
| 321 |
-
lines.forEach(line => {
|
| 322 |
-
const match = line.match(taskRegex);
|
| 323 |
-
if (match) {
|
| 324 |
-
const completed = match[1] === 'x';
|
| 325 |
-
const title = match[2].trim();
|
| 326 |
-
const timePart = match[3] ? match[3].trim() : '0h';
|
| 327 |
-
const tagsPart = match[4] ? match[4].trim() : '';
|
| 328 |
-
|
| 329 |
-
// Parse time
|
| 330 |
-
const timeMatch = timePart.match(/(\d+)(?:\s*h\s*)?(?:(?:\s*(\d+)\s*m\s*)?)?/);
|
| 331 |
-
const hours = parseInt(timeMatch[1]) || 0;
|
| 332 |
-
const minutes = parseInt(timeMatch[2]) || 0;
|
| 333 |
-
const estimatedTime = hours * 60 + minutes;
|
| 334 |
-
|
| 335 |
-
tasks.push({
|
| 336 |
-
title,
|
| 337 |
-
completed,
|
| 338 |
-
estimatedTime
|
| 339 |
-
});
|
| 340 |
-
}
|
| 341 |
-
});
|
| 342 |
-
return tasks;
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
function generateMarkdown() {
|
| 346 |
-
return state.tasks.map(task => {
|
| 347 |
-
const status = task.completed ? 'x' : ' ';
|
| 348 |
-
const hours = Math.floor(task.estimatedTime / 60);
|
| 349 |
-
const minutes = task.estimatedTime % 60;
|
| 350 |
-
const timeStr = `${hours}h${minutes > 0 ? ` ${minutes}m` : ''}`;
|
| 351 |
-
return `- [${status}] ${task.title} | ${timeStr}`;
|
| 352 |
-
}).join('\n');
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
// Rendering
|
| 356 |
-
function render() {
|
| 357 |
-
renderTasks();
|
| 358 |
-
renderProgressBars();
|
| 359 |
-
renderTags();
|
| 360 |
-
renderTimeline();
|
| 361 |
-
}
|
| 362 |
-
function renderTasks() {
|
| 363 |
-
const openContainer = document.getElementById('open-tasks');
|
| 364 |
-
const completedContainer = document.getElementById('completed-tasks');
|
| 365 |
-
|
| 366 |
-
const openTasks = state.tasks.filter(t => !t.completed);
|
| 367 |
-
const completedTasks = state.tasks.filter(t => t.completed);
|
| 368 |
-
|
| 369 |
-
// Render open tasks
|
| 370 |
-
if (openTasks.length === 0) {
|
| 371 |
-
openContainer.innerHTML = '<div class="empty-state"><i data-feather="inbox"></i><p>Keine offenen Aufgaben</p></div>';
|
| 372 |
-
} else {
|
| 373 |
-
openContainer.innerHTML = '';
|
| 374 |
-
openTasks.forEach(task => {
|
| 375 |
-
const taskElement = document.createElement('task-item');
|
| 376 |
-
taskElement.setAttribute('task-id', task.id);
|
| 377 |
-
taskElement.setAttribute('completed', 'false');
|
| 378 |
-
openContainer.appendChild(taskElement);
|
| 379 |
-
});
|
| 380 |
}
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
if (completedTasks.length === 0) {
|
| 384 |
-
completedContainer.innerHTML = '<div class="empty-state"><i data-feather="check-circle"></i><p>Keine erledigten Aufgaben</p></div>';
|
| 385 |
-
} else {
|
| 386 |
-
completedContainer.innerHTML = '';
|
| 387 |
-
completedTasks.forEach(task => {
|
| 388 |
-
const taskElement = document.createElement('task-item');
|
| 389 |
-
taskElement.setAttribute('task-id', task.id);
|
| 390 |
-
taskElement.setAttribute('completed', 'true');
|
| 391 |
-
completedContainer.appendChild(taskElement);
|
| 392 |
-
});
|
| 393 |
-
}
|
| 394 |
-
|
| 395 |
-
document.getElementById('open-count').textContent = openTasks.length;
|
| 396 |
-
document.getElementById('completed-count').textContent = completedTasks.length;
|
| 397 |
-
|
| 398 |
-
// Setup drag and drop for task items
|
| 399 |
-
setupTaskDragAndDrop();
|
| 400 |
-
|
| 401 |
-
// Replace feather icons
|
| 402 |
-
feather.replace();
|
| 403 |
-
}
|
| 404 |
-
function renderProgressBars() {
|
| 405 |
-
// Overall progress
|
| 406 |
-
const totalEstimated = state.tasks.reduce((sum, t) => sum + t.estimatedTime, 0) || 1;
|
| 407 |
-
const totalSpent = state.tasks.reduce((sum, t) => sum + t.spentTime, 0);
|
| 408 |
-
const totalProgress = (totalSpent / totalEstimated) * 100;
|
| 409 |
-
|
| 410 |
-
document.getElementById('total-progress-bar').style.width = `${totalProgress}%`;
|
| 411 |
-
document.getElementById('total-progress-text').textContent =
|
| 412 |
-
`${Math.round(totalProgress)}% (${formatTime(totalSpent)} / ${formatTime(totalEstimated)})`;
|
| 413 |
-
|
| 414 |
-
// Tag progress bars - only for tags with tasks
|
| 415 |
-
const container = document.getElementById('tag-progress-container');
|
| 416 |
-
container.innerHTML = state.tags
|
| 417 |
-
.filter(tag => state.tasks.some(t => t.tags.includes(tag.id))) // Only tags with tasks
|
| 418 |
-
.map(tag => {
|
| 419 |
-
const tagTasks = state.tasks.filter(t => t.tags.includes(tag.id));
|
| 420 |
-
const tagEstimated = tagTasks.reduce((sum, t) => sum + t.estimatedTime, 0) || 1;
|
| 421 |
-
const tagSpent = tagTasks.reduce((sum, t) => sum + t.spentTime, 0);
|
| 422 |
-
const tagProgress = (tagSpent / tagEstimated) * 100;
|
| 423 |
-
|
| 424 |
-
return `
|
| 425 |
-
<div class="bg-gray-800 rounded-lg p-4 shadow transition-all hover:scale-[1.02]">
|
| 426 |
-
<div class="flex items-center justify-between mb-2">
|
| 427 |
-
<span class="font-medium" style="color: ${tag.color}">${tag.name}</span>
|
| 428 |
-
<span class="text-sm text-gray-400">
|
| 429 |
-
${Math.round(tagProgress)}% (${formatTime(tagSpent)} / ${formatTime(tagEstimated)})
|
| 430 |
-
</span>
|
| 431 |
-
</div>
|
| 432 |
-
<div class="w-full bg-gray-700 rounded-full h-2 overflow-hidden">
|
| 433 |
-
<div class="h-full rounded-full transition-all duration-500"
|
| 434 |
-
style="width: ${tagProgress}%; background-color: ${tag.color}"></div>
|
| 435 |
-
</div>
|
| 436 |
-
</div>
|
| 437 |
-
`;
|
| 438 |
-
}).join('');
|
| 439 |
-
}
|
| 440 |
-
function renderTags() {
|
| 441 |
-
const container = document.getElementById('tags-list');
|
| 442 |
-
container.innerHTML = state.tags.map(tag => `
|
| 443 |
-
<tag-item tag-id="${tag.id}" tag-name="${tag.name}" tag-color="${tag.color}"></tag-item>
|
| 444 |
-
`).join('');
|
| 445 |
-
|
| 446 |
-
// Render tag components
|
| 447 |
-
document.querySelectorAll('tag-item').forEach(el => {
|
| 448 |
-
const tagId = el.getAttribute('tag-id');
|
| 449 |
-
el.addEventListener('delete', () => deleteTag(tagId));
|
| 450 |
-
});
|
| 451 |
-
}
|
| 452 |
-
function highlightCurrentHour() {
|
| 453 |
-
const now = new Date();
|
| 454 |
-
const currentHour = now.getHours();
|
| 455 |
-
const currentMinute = now.getMinutes();
|
| 456 |
-
|
| 457 |
-
// Highlight current hour
|
| 458 |
-
document.querySelectorAll('.timeline-hour').forEach(el => {
|
| 459 |
-
el.classList.remove('bg-gray-750');
|
| 460 |
-
});
|
| 461 |
-
|
| 462 |
-
const hourElement = document.querySelector(`.timeline-hour[data-hour="${currentHour}"]`);
|
| 463 |
-
if (hourElement) {
|
| 464 |
-
hourElement.classList.add('bg-gray-750');
|
| 465 |
}
|
| 466 |
-
|
| 467 |
-
// Add current time indicator line
|
| 468 |
-
document.getElementById('current-time-indicator')?.remove();
|
| 469 |
-
|
| 470 |
-
const timelineContainer = document.getElementById('timeline-hours');
|
| 471 |
-
if (timelineContainer) {
|
| 472 |
-
const indicator = document.createElement('div');
|
| 473 |
-
indicator.id = 'current-time-indicator';
|
| 474 |
-
indicator.style.position = 'absolute';
|
| 475 |
-
indicator.style.left = '0';
|
| 476 |
-
indicator.style.right = '0';
|
| 477 |
-
indicator.style.height = '2px';
|
| 478 |
-
indicator.style.backgroundColor = '#ef4444';
|
| 479 |
-
indicator.style.zIndex = '10';
|
| 480 |
-
|
| 481 |
-
const hourElement = document.querySelector(`.timeline-hour[data-hour="${currentHour}"]`);
|
| 482 |
-
if (hourElement) {
|
| 483 |
-
const hourTop = hourElement.offsetTop;
|
| 484 |
-
const hourHeight = hourElement.offsetHeight;
|
| 485 |
-
const minutePosition = (currentMinute / 60) * hourHeight;
|
| 486 |
-
|
| 487 |
-
indicator.style.top = `${hourTop + minutePosition}px`;
|
| 488 |
-
timelineContainer.appendChild(indicator);
|
| 489 |
-
|
| 490 |
-
// Add time label
|
| 491 |
-
const timeLabel = document.createElement('div');
|
| 492 |
-
timeLabel.textContent = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
| 493 |
-
timeLabel.style.position = 'absolute';
|
| 494 |
-
timeLabel.style.left = '-64px';
|
| 495 |
-
timeLabel.style.top = `${hourTop + minutePosition - 10}px`;
|
| 496 |
-
timeLabel.style.backgroundColor = '#ef4444';
|
| 497 |
-
timeLabel.style.color = 'white';
|
| 498 |
-
timeLabel.style.padding = '2px 8px';
|
| 499 |
-
timeLabel.style.borderRadius = '4px';
|
| 500 |
-
timeLabel.style.fontSize = '12px';
|
| 501 |
-
timeLabel.style.zIndex = '10';
|
| 502 |
-
|
| 503 |
-
hourEl.appendChild(timeLabel);
|
| 504 |
}
|
| 505 |
-
|
|
|
|
|
|
|
| 506 |
}
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
if (hourEl) {
|
| 513 |
-
hourEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 514 |
-
}
|
| 515 |
}
|
| 516 |
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
if (openTasks.length === 0) {
|
| 522 |
-
alert('Keine offenen Aufgaben für den Fokus-Modus!');
|
| 523 |
-
return;
|
| 524 |
-
}
|
| 525 |
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
<div class="space-y-2">
|
| 532 |
-
${openTasks.map(task => `
|
| 533 |
-
<div class="bg-gray-700 p-3 rounded-lg cursor-pointer hover:bg-gray-600 transition"
|
| 534 |
-
onclick="startFocusModeWithId('${task.id}')">
|
| 535 |
-
<div class="font-medium">${task.title}</div>
|
| 536 |
-
<div class="text-sm text-gray-400">${formatTime(task.estimatedTime)}</div>
|
| 537 |
-
</div>
|
| 538 |
-
`).join('')}
|
| 539 |
-
</div>
|
| 540 |
-
<button onclick="this.closest('div[class*=\"fixed\"]').remove()"
|
| 541 |
-
class="mt-4 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition w-full">
|
| 542 |
-
Abbrechen
|
| 543 |
-
</button>
|
| 544 |
-
</div>
|
| 545 |
-
`;
|
| 546 |
-
document.body.appendChild(modal);
|
| 547 |
-
}
|
| 548 |
-
|
| 549 |
-
function startFocusModeWithId(taskId) {
|
| 550 |
-
const task = state.tasks.find(t => t.id === taskId);
|
| 551 |
-
if (task) {
|
| 552 |
-
document.querySelector('div[class*="fixed"]')?.remove();
|
| 553 |
-
startFocusMode(task);
|
| 554 |
-
}
|
| 555 |
}
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
document.getElementById('focus-mode').classList.remove('hidden');
|
| 560 |
-
|
| 561 |
-
// Start pomodoro
|
| 562 |
-
const workTime = parseInt(localStorage.getItem('pomodoroWorkTime')) || 25;
|
| 563 |
-
startPomodoro(workTime * 60, false);
|
| 564 |
-
}
|
| 565 |
-
function startPomodoro(seconds, isBreak) {
|
| 566 |
-
state.pomodoro.isRunning = true;
|
| 567 |
-
state.pomodoro.isBreak = isBreak;
|
| 568 |
-
state.pomodoro.timeRemaining = seconds;
|
| 569 |
-
state.pomodoro.totalTime = seconds;
|
| 570 |
-
|
| 571 |
-
clearInterval(state.pomodoro.interval);
|
| 572 |
-
|
| 573 |
-
state.pomodoro.interval = setInterval(() => {
|
| 574 |
-
if (state.pomodoro.timeRemaining > 0) {
|
| 575 |
-
state.pomodoro.timeRemaining--;
|
| 576 |
-
updatePomodoroDisplay();
|
| 577 |
-
} else {
|
| 578 |
-
handlePomodoroComplete();
|
| 579 |
-
}
|
| 580 |
-
}, 1000);
|
| 581 |
-
|
| 582 |
-
updatePomodoroDisplay();
|
| 583 |
}
|
| 584 |
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
const seconds = state.pomodoro.timeRemaining % 60;
|
| 588 |
-
document.getElementById('pomodoro-timer').textContent =
|
| 589 |
-
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
| 590 |
-
|
| 591 |
-
const statusEl = document.getElementById('pomodoro-status');
|
| 592 |
-
statusEl.textContent = state.pomodoro.isBreak ? 'Pausenzeit läuft...' : 'Arbeitszeit läuft...';
|
| 593 |
}
|
| 594 |
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
if (!state.pomodoro.isBreak) {
|
| 599 |
-
// Work session complete
|
| 600 |
-
const elapsed = state.pomodoro.totalTime;
|
| 601 |
-
state.focusTask.spentTime += elapsed;
|
| 602 |
-
saveToLocalStorage();
|
| 603 |
-
|
| 604 |
-
// Start break
|
| 605 |
-
const breakTime = parseInt(localStorage.getItem('pomodoroBreakTime')) || 5;
|
| 606 |
-
startPomodoro(breakTime * 60, true);
|
| 607 |
-
|
| 608 |
-
// Notification
|
| 609 |
-
if ('Notification' in window && Notification.permission === 'granted') {
|
| 610 |
-
new Notification('Pause!', { body: 'Arbeitszeit abgeschlossen. Zeit für eine Pause!' });
|
| 611 |
-
}
|
| 612 |
-
} else {
|
| 613 |
-
// Break complete
|
| 614 |
-
if ('Notification' in window && Notification.permission === 'granted') {
|
| 615 |
-
new Notification('Weiterarbeiten!', { body: 'Pausenzeit ist vorbei.' });
|
| 616 |
-
}
|
| 617 |
-
}
|
| 618 |
}
|
| 619 |
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
document.getElementById('focus-pause').classList.add('hidden');
|
| 624 |
-
document.getElementById('focus-play').classList.remove('hidden');
|
| 625 |
}
|
| 626 |
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
}
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
}
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
document.getElementById('focus-mode').classList.add('hidden');
|
| 645 |
-
render();
|
| 646 |
-
}
|
| 647 |
-
function savePomodoroSettings() {
|
| 648 |
-
localStorage.setItem('pomodoroWorkTime', document.getElementById('pomodoro-work').value);
|
| 649 |
-
localStorage.setItem('pomodoroBreakTime', document.getElementById('pomodoro-break').value);
|
| 650 |
-
}
|
| 651 |
-
// Timeline Functions
|
| 652 |
-
function initializeTimeline() {
|
| 653 |
-
// Setup drag start for task items
|
| 654 |
-
document.addEventListener('dragstart', (e) => {
|
| 655 |
-
if (e.target.tagName === 'TASK-ITEM') {
|
| 656 |
-
const taskId = e.target.getAttribute('task-id');
|
| 657 |
-
e.dataTransfer.setData('text/plain', taskId);
|
| 658 |
-
}
|
| 659 |
-
});
|
| 660 |
-
}
|
| 661 |
-
function handleDragStart(e) {
|
| 662 |
-
if (e.target.classList.contains('task-item')) {
|
| 663 |
-
const taskId = e.target.getAttribute('task-id');
|
| 664 |
-
e.dataTransfer.setData('text/plain', taskId);
|
| 665 |
-
e.target.classList.add('dragging');
|
| 666 |
}
|
| 667 |
}
|
| 668 |
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
if (e.target.classList.contains('timeline-hour')) {
|
| 673 |
-
e.target.classList.add('drag-over');
|
| 674 |
-
}
|
| 675 |
}
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
|
|
|
| 681 |
}
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
|
| 703 |
-
|
|
|
|
|
|
|
| 704 |
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
|
|
|
| 709 |
}
|
| 710 |
}
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
return `${h}h${m > 0 ? ` ${m}m` : ''}`;
|
| 716 |
}
|
| 717 |
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
|
|
|
| 721 |
}
|
| 722 |
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
render();
|
| 730 |
}
|
| 731 |
}
|
| 732 |
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
const draggable = document.querySelector('.dragging');
|
| 745 |
-
if (draggable) {
|
| 746 |
-
if (afterElement == null) {
|
| 747 |
-
container.appendChild(draggable);
|
| 748 |
-
} else {
|
| 749 |
-
container.insertBefore(draggable, afterElement);
|
| 750 |
-
}
|
| 751 |
-
});
|
| 752 |
-
});
|
| 753 |
-
}
|
| 754 |
-
|
| 755 |
-
function getDragAfterElement(container, y) {
|
| 756 |
-
const draggableElements = [...container.querySelectorAll('task-item:not(.dragging)'));
|
| 757 |
-
|
| 758 |
-
return draggableElements.reduce((closest, child) => {
|
| 759 |
-
const box = child.getBoundingClientRect();
|
| 760 |
-
const offset = y - box.top - box.height / 2;
|
| 761 |
-
if (offset < 0 && offset > closest.offset) {
|
| 762 |
-
return { offset: offset, element: child };
|
| 763 |
-
} else {
|
| 764 |
-
return closest;
|
| 765 |
-
}
|
| 766 |
-
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
| 767 |
-
}
|
| 768 |
-
|
| 769 |
-
// Edit task modal functionality
|
| 770 |
-
function openEditTaskModal(taskId) {
|
| 771 |
-
const task = state.tasks.find(t => t.id === taskId);
|
| 772 |
-
if (!task) return;
|
| 773 |
-
|
| 774 |
-
const modal = document.createElement('div');
|
| 775 |
-
modal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
|
| 776 |
-
modal.innerHTML = `
|
| 777 |
-
<div class="bg-gray-800 rounded-xl p-6 max-w-md w-full">
|
| 778 |
-
<h3 class="text-xl font-semibold mb-4">Aufgabe bearbeiten</h3>
|
| 779 |
-
<form id="edit-task-form" class="space-y-4">
|
| 780 |
-
<div>
|
| 781 |
-
<label class="block text-sm font-medium mb-2">Titel</label>
|
| 782 |
-
<input id="edit-task-title" type="text" required value="${task.title}"
|
| 783 |
-
class="w-full bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 784 |
-
</div>
|
| 785 |
-
<div>
|
| 786 |
-
<label class="block text-sm font-medium mb-2">Geschätzte Zeit</label>
|
| 787 |
-
<div class="flex space-x-2">
|
| 788 |
-
<input id="edit-task-hours" type="number" placeholder="h" min="0" max="24" value="${Math.floor(task.estimatedTime / 60)}"
|
| 789 |
-
class="w-full bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 790 |
-
<input id="edit-task-minutes" type="number" placeholder="m" min="0" max="59" step="15" value="${task.estimatedTime % 60}"
|
| 791 |
-
class="w-full bg-gray-700 rounded-lg px-4 py-2 text-gray-100">
|
| 792 |
-
</div>
|
| 793 |
-
</div>
|
| 794 |
-
<div class="flex justify-end space-x-2">
|
| 795 |
-
<button type="button" onclick="this.closest('div[class*=\"fixed\"]').remove()"
|
| 796 |
-
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition">
|
| 797 |
-
Abbrechen
|
| 798 |
-
</button>
|
| 799 |
-
<button type="submit" class="px-4 py-2 bg-orange-500 hover:bg-orange-600 rounded-lg transition">
|
| 800 |
-
Speichern
|
| 801 |
-
</button>
|
| 802 |
-
</div>
|
| 803 |
-
</form>
|
| 804 |
-
</div>
|
| 805 |
-
`;
|
| 806 |
-
|
| 807 |
-
document.body.appendChild(modal);
|
| 808 |
-
|
| 809 |
-
const form = modal.querySelector('#edit-task-form');
|
| 810 |
-
form.addEventListener('submit', (e) => {
|
| 811 |
-
e.preventDefault();
|
| 812 |
-
handleEditTask(taskId);
|
| 813 |
-
modal.remove();
|
| 814 |
-
});
|
| 815 |
-
}
|
| 816 |
-
|
| 817 |
-
function handleEditTask(taskId) {
|
| 818 |
-
const title = document.getElementById('edit-task-title').value;
|
| 819 |
-
const hours = parseInt(document.getElementById('edit-task-hours').value) || 0;
|
| 820 |
-
const minutes = parseInt(document.getElementById('edit-task-minutes').value) || 0;
|
| 821 |
-
const estimatedTime = hours * 60 + minutes;
|
| 822 |
-
|
| 823 |
-
updateTask(taskId, { title, estimatedTime });
|
| 824 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Custom styles for TaskForge Pro */
|
| 2 |
|
| 3 |
+
/* Scrollbar styling */
|
| 4 |
+
::-webkit-scrollbar {
|
| 5 |
+
width: 8px;
|
| 6 |
+
height: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
}
|
| 8 |
|
| 9 |
+
::-webkit-scrollbar-track {
|
| 10 |
+
@apply bg-gray-800;
|
|
|
|
|
|
|
| 11 |
}
|
| 12 |
|
| 13 |
+
::-webkit-scrollbar-thumb {
|
| 14 |
+
@apply bg-gray-600 rounded-full;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
+
::-webkit-scrollbar-thumb:hover {
|
| 18 |
+
@apply bg-gray-500;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
}
|
| 20 |
+
/* Task hover effects */
|
| 21 |
+
.task-item {
|
| 22 |
+
@apply relative overflow-hidden;
|
| 23 |
+
transition: all 0.3s ease;
|
| 24 |
+
border-left: 4px solid transparent;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
+
|
| 27 |
+
.task-item.completed {
|
| 28 |
+
animation: completeTask 0.6s ease forwards;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
+
@keyframes completeTask {
|
| 32 |
+
0% { transform: scale(1); opacity: 1; }
|
| 33 |
+
50% { transform: scale(0.95); opacity: 0.8; }
|
| 34 |
+
100% { transform: scale(1); opacity: 1; border-left-color: #10b981; }
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
+
.task-item .focus-btn {
|
| 38 |
+
@apply absolute right-12 top-1/2 transform -translate-y-1/2;
|
| 39 |
+
@apply p-2 rounded-full bg-orange-500 bg-opacity-0 text-orange-500;
|
| 40 |
+
@apply transition-all duration-300 ease-in-out;
|
| 41 |
+
opacity: 0;
|
| 42 |
}
|
| 43 |
+
|
| 44 |
+
.task-item:hover .focus-btn {
|
| 45 |
+
@apply bg-opacity-20;
|
| 46 |
+
opacity: 1;
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
+
|
| 49 |
+
.task-item .focus-btn:hover {
|
| 50 |
+
@apply bg-opacity-100 text-white;
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
+
.task-item:hover {
|
| 53 |
+
@apply transform scale-105 shadow-lg;
|
|
|
|
|
|
|
| 54 |
}
|
| 55 |
|
| 56 |
+
/* Progress bar animation */
|
| 57 |
+
.progress-bar-fill {
|
| 58 |
+
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
+
/* Focus mode animations */
|
| 62 |
+
@keyframes pulse {
|
| 63 |
+
0%, 100% {
|
| 64 |
+
opacity: 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
+
50% {
|
| 67 |
+
opacity: 0.5;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
}
|
| 70 |
+
|
| 71 |
+
.focus-pulse {
|
| 72 |
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
| 73 |
}
|
| 74 |
+
|
| 75 |
+
/* Button focus styles */
|
| 76 |
+
.btn:focus {
|
| 77 |
+
@apply outline-none ring-2 ring-orange-500 ring-offset-2 ring-offset-gray-900;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
}
|
| 79 |
|
| 80 |
+
/* Tag badge styles */
|
| 81 |
+
.tag-badge {
|
| 82 |
+
@apply inline-flex items-center px-2 py-1 rounded-full text-xs font-medium;
|
| 83 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
/* Timeline styles */
|
| 86 |
+
.timeline-hour {
|
| 87 |
+
@apply relative border-b border-gray-700;
|
| 88 |
+
min-height: 60px;
|
| 89 |
+
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
+
.timeline-task-block {
|
| 92 |
+
@apply absolute rounded-lg cursor-move transition-all;
|
| 93 |
+
transition: all 0.2s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
|
| 96 |
+
.timeline-task-block:hover {
|
| 97 |
+
@apply transform scale-105 shadow-lg;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
+
.timeline-task-block.dragging {
|
| 101 |
+
@apply opacity-50;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
|
| 104 |
+
/* Modal backdrop blur */
|
| 105 |
+
.modal-backdrop {
|
| 106 |
+
backdrop-filter: blur(4px);
|
|
|
|
|
|
|
| 107 |
}
|
| 108 |
|
| 109 |
+
/* Custom input styles */
|
| 110 |
+
input[type="text"], input[type="number"], textarea, select {
|
| 111 |
+
@apply bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-gray-100;
|
| 112 |
+
@apply focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent;
|
| 113 |
}
|
| 114 |
+
|
| 115 |
+
input[type="color"] {
|
| 116 |
+
@apply cursor-pointer;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/* Navigation buttons */
|
| 120 |
+
.nav-btn {
|
| 121 |
+
@apply flex items-center space-x-2 px-4 py-2 rounded-lg transition;
|
| 122 |
+
@apply text-gray-400 hover:text-white hover:bg-gray-700;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.nav-btn.active {
|
| 126 |
+
@apply text-orange-500 bg-gray-700;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/* View transitions */
|
| 130 |
+
.view {
|
| 131 |
+
display: none;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.view.active {
|
| 135 |
+
display: block;
|
| 136 |
+
animation: fadeIn 0.3s ease-in-out;
|
| 137 |
+
}
|
| 138 |
+
@keyframes fadeIn {
|
| 139 |
+
from {
|
| 140 |
+
opacity: 0;
|
| 141 |
+
transform: translateY(10px);
|
| 142 |
}
|
| 143 |
+
to {
|
| 144 |
+
opacity: 1;
|
| 145 |
+
transform: translateY(0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
}
|
| 147 |
}
|
| 148 |
|
| 149 |
+
/* Drag and drop styles */
|
| 150 |
+
.drag-over {
|
| 151 |
+
@apply bg-gray-700 border-2 border-dashed border-orange-500;
|
|
|
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
+
|
| 154 |
+
/* Custom checkbox */
|
| 155 |
+
.custom-checkbox {
|
| 156 |
+
@apply w-5 h-5 rounded cursor-pointer appearance-none;
|
| 157 |
+
@apply bg-gray-700 border border-gray-600;
|
| 158 |
+
@apply checked:bg-emerald-500 checked:border-emerald-500;
|
| 159 |
}
|
| 160 |
+
|
| 161 |
+
.custom-checkbox:checked::after {
|
| 162 |
+
content: '✓';
|
| 163 |
+
@apply block text-white text-center leading-5;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* Time display */
|
| 167 |
+
.time-display {
|
| 168 |
+
@apply font-mono text-lg tabular-nums;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
/* Empty state */
|
| 172 |
+
.empty-state {
|
| 173 |
+
@apply text-center py-12 text-gray-500;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.empty-state i {
|
| 177 |
+
@apply w-16 h-16 mx-auto mb-4;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/* Responsive timeline */
|
| 181 |
+
@media (max-width: 768px) {
|
| 182 |
+
#timeline-tasks {
|
| 183 |
+
width: 100%;
|
| 184 |
+
position: static;
|
| 185 |
+
}
|
| 186 |
|
| 187 |
+
#timeline-hours {
|
| 188 |
+
margin-left: 0;
|
| 189 |
+
}
|
| 190 |
|
| 191 |
+
.timeline-task-block {
|
| 192 |
+
position: relative !important;
|
| 193 |
+
width: 100% !important;
|
| 194 |
+
left: 0 !important;
|
| 195 |
+
top: auto !important;
|
| 196 |
}
|
| 197 |
}
|
| 198 |
+
|
| 199 |
+
/* Loading animation */
|
| 200 |
+
.loading {
|
| 201 |
+
@apply animate-pulse;
|
|
|
|
| 202 |
}
|
| 203 |
|
| 204 |
+
.loading::after {
|
| 205 |
+
content: '';
|
| 206 |
+
@apply absolute inset-0 bg-gray-700 rounded-lg;
|
| 207 |
+
animation: loading 1.5s ease-in-out infinite;
|
| 208 |
}
|
| 209 |
|
| 210 |
+
@keyframes loading {
|
| 211 |
+
0% {
|
| 212 |
+
transform: translateX(-100%);
|
| 213 |
+
}
|
| 214 |
+
100% {
|
| 215 |
+
transform: translateX(100%);
|
|
|
|
| 216 |
}
|
| 217 |
}
|
| 218 |
|
| 219 |
+
/* Task priority indicator */
|
| 220 |
+
.priority-high {
|
| 221 |
+
@apply border-l-4 border-red-500;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.priority-medium {
|
| 225 |
+
@apply border-l-4 border-yellow-500;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.priority-low {
|
| 229 |
+
@apply border-l-4 border-emerald-500;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
}
|
| 231 |
+
|
| 232 |
+
/* Focus mode glass effect */
|
| 233 |
+
#focus-mode {
|
| 234 |
+
background: linear-gradient(135deg, rgba(17, 24, 39, 0.95) 0%, rgba(17, 24, 39, 0.98) 100%);
|
| 235 |
+
backdrop-filter: blur(10px);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
/* Smooth transitions for all interactive elements */
|
| 239 |
+
* {
|
| 240 |
+
transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
|
| 241 |
+
}
|