Spaces:
Running
Running
| // Task Item Component | |
| class TaskItem extends HTMLElement { | |
| constructor() { | |
| super(); | |
| this.attachShadow({ mode: 'open' }); | |
| } | |
| connectedCallback() { | |
| this.taskId = this.getAttribute('task-id'); | |
| this.completed = this.getAttribute('completed') === 'true'; | |
| this.loadTaskData(); | |
| this.render(); | |
| this.setupEventListeners(); | |
| } | |
| static get observedAttributes() { | |
| return ['task-id', 'completed']; | |
| } | |
| attributeChangedCallback(name, oldValue, newValue) { | |
| if (name === 'task-id' && oldValue !== newValue) { | |
| this.taskId = newValue; | |
| this.loadTaskData(); | |
| } else if (name === 'completed') { | |
| this.completed = newValue === 'true'; | |
| } | |
| this.render(); | |
| } | |
| loadTaskData() { | |
| if (!window.state || !window.state.tasks) return; | |
| this.task = window.state.tasks.find(t => t.id === this.taskId); | |
| this.tags = window.state.tags || []; | |
| } | |
| render() { | |
| if (!this.task) { | |
| this.shadowRoot.innerHTML = `<div class="loading" data-task-id="${this.taskId}">Lädt...</div>`; | |
| return; | |
| } | |
| const { title, estimatedTime, spentTime, tags } = this.task; | |
| const progress = estimatedTime > 0 ? (spentTime / estimatedTime) * 100 : 0; | |
| const tagElements = (tags || []).map(tagId => { | |
| const tag = this.tags?.find(t => t.id === tagId); | |
| return tag ? `<span class="tag-badge" style="background-color: ${tag.color}20; color: ${tag.color}">${tag.name}</span>` : ''; | |
| }).join(''); | |
| this.shadowRoot.innerHTML = ` | |
| <style> | |
| :host { | |
| display: block; | |
| background: #1f2937; | |
| border-radius: 8px; | |
| padding: 12px; | |
| margin-bottom: 8px; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| } | |
| :host(:hover) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); | |
| } | |
| :host(.completed) { | |
| opacity: 0.6; | |
| } | |
| .task-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 8px; | |
| } | |
| .task-title { | |
| font-size: 16px; | |
| font-weight: 500; | |
| color: #f3f4f6; | |
| flex: 1; | |
| margin-left: 8px; | |
| text-decoration: none; | |
| } | |
| :host(.completed) .task-title { | |
| text-decoration: line-through; | |
| color: #9ca3af; | |
| } | |
| .task-checkbox { | |
| width: 20px; | |
| height: 20px; | |
| cursor: pointer; | |
| } | |
| .task-actions { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .task-action { | |
| background: none; | |
| border: none; | |
| color: #6b7280; | |
| cursor: pointer; | |
| padding: 4px; | |
| transition: color 0.2s ease; | |
| } | |
| .task-action:hover { | |
| color: #f97316; | |
| } | |
| .task-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| margin-top: 8px; | |
| } | |
| .time-info { | |
| font-size: 14px; | |
| color: #9ca3af; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 4px; | |
| background: #374151; | |
| border-radius: 2px; | |
| overflow: hidden; | |
| margin-top: 8px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #f97316, #ea580c); | |
| transition: width 0.3s ease; | |
| } | |
| .tags { | |
| display: flex; | |
| gap: 4px; | |
| flex-wrap: wrap; | |
| } | |
| .tag-badge { | |
| font-size: 12px; | |
| padding: 2px 8px; | |
| border-radius: 12px; | |
| font-weight: 500; | |
| } | |
| .edit-form { | |
| display: none; | |
| margin-top: 12px; | |
| padding-top: 12px; | |
| border-top: 1px solid #374151; | |
| } | |
| .edit-form.active { | |
| display: block; | |
| } | |
| .input-group { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| } | |
| .input-group input { | |
| background: #374151; | |
| border: 1px solid #4b5563; | |
| border-radius: 4px; | |
| padding: 6px 12px; | |
| color: #f3f4f6; | |
| font-size: 14px; | |
| } | |
| .input-group input:focus { | |
| outline: none; | |
| border-color: #f97316; | |
| } | |
| .edit-actions { | |
| display: flex; | |
| gap: 8px; | |
| justify-content: flex-end; | |
| } | |
| .btn-small { | |
| padding: 6px 12px; | |
| border-radius: 4px; | |
| border: none; | |
| font-size: 14px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .btn-save { | |
| background: #059669; | |
| color: white; | |
| } | |
| .btn-save:hover { | |
| background: #047857; | |
| } | |
| .btn-cancel { | |
| background: #374151; | |
| color: #d1d5db; | |
| } | |
| .btn-cancel:hover { | |
| background: #4b5563; | |
| } | |
| </style> | |
| <div class="task-header"> | |
| <input type="checkbox" class="task-checkbox" ${this.completed ? 'checked' : ''} | |
| @change="${this.handleToggleComplete}"> | |
| <span class="task-title">${title}</span> | |
| <div class="task-actions"> | |
| <button class="focus-btn" @click="${this.handleFocus}" title="Pomodoro starten"> | |
| <i data-feather="clock"></i> | |
| </button> | |
| <button class="task-action" @click="${this.handleEdit}" title="Bearbeiten"> | |
| <i data-feather="edit-2"></i> | |
| </button> | |
| <button class="task-action" @click="${this.handleDelete}" title="Löschen"> | |
| <i data-feather="trash-2"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="task-info"> | |
| <span class="time-info">${formatTime(spentTime)} / ${formatTime(estimatedTime)}</span> | |
| <div class="tags">${tagElements}</div> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" style="width: ${progress}%"></div> | |
| </div> | |
| <div class="edit-form" id="edit-form-${this.taskId}"> | |
| <div class="input-group"> | |
| <input type="text" value="${title}" id="edit-title-${this.taskId}" placeholder="Titel"> | |
| <input type="number" value="${Math.floor(estimatedTime / 60)}" id="edit-hours-${this.taskId}" | |
| placeholder="h" min="0" max="24" style="width: 60px"> | |
| <input type="number" value="${estimatedTime % 60}" id="edit-minutes-${this.taskId}" | |
| placeholder="m" min="0" max="59" step="15" style="width: 60px"> | |
| <button class="btn-small btn-add-time" @click="${this.handleAddTime}" title="Zeit hinzufügen"> | |
| <i data-feather="clock" style="width: 14px; height: 14px;"></i> | |
| </button> | |
| </div> | |
| <div class="edit-actions"> | |
| <button class="btn-small btn-save" @click="${this.handleSave}">Speichern</button> | |
| <button class="btn-small btn-cancel" @click="${this.handleCancel}">Abbrechen</button> | |
| </div> | |
| </div> | |
| `; | |
| if (this.completed) { | |
| this.classList.add('completed'); | |
| } else { | |
| this.classList.remove('completed'); | |
| } | |
| // Replace feather icons | |
| this.shadowRoot.querySelectorAll('i[data-feather]').forEach(icon => { | |
| const iconName = icon.getAttribute('data-feather'); | |
| icon.outerHTML = feather.icons[iconName].toSvg({ width: 16, height: 16 }); | |
| }); | |
| } | |
| setupEventListeners() { | |
| this.shadowRoot.addEventListener('click', (e) => { | |
| const action = e.target.closest('button')?.getAttribute('@click'); | |
| if (action) { | |
| e.stopPropagation(); | |
| this[action.replace('${this.', '').replace('}', '')](e); | |
| } | |
| }); | |
| this.shadowRoot.addEventListener('change', (e) => { | |
| if (e.target.classList.contains('task-checkbox')) { | |
| e.stopPropagation(); | |
| this.handleToggleComplete(e); | |
| } | |
| }); | |
| } | |
| handleToggleComplete(e) { | |
| const completed = e.target.checked; | |
| this.dispatchEvent(new CustomEvent('toggle-complete', { | |
| detail: { taskId: this.getAttribute('task-id'), completed }, | |
| bubbles: true | |
| })); | |
| if (completed) { | |
| this.classList.add('completed'); | |
| } else { | |
| this.classList.remove('completed'); | |
| } | |
| } | |
| handleFocus(e) { | |
| const taskId = this.getAttribute('task-id'); | |
| const task = state.tasks.find(t => t.id === taskId); | |
| if (task) { | |
| startFocusMode(task); | |
| } | |
| } | |
| handleEdit(e) { | |
| const editForm = this.shadowRoot.querySelector('.edit-form'); | |
| editForm.classList.add('active'); | |
| } | |
| handleDelete(e) { | |
| if (confirm('Aufgabe wirklich löschen?')) { | |
| this.dispatchEvent(new CustomEvent('delete-task', { | |
| detail: { taskId: this.getAttribute('task-id') }, | |
| bubbles: true | |
| })); | |
| } | |
| } | |
| handleSave(e) { | |
| const taskId = this.getAttribute('task-id'); | |
| const title = this.shadowRoot.getElementById(`edit-title-${taskId}`).value; | |
| const hours = parseInt(this.shadowRoot.getElementById(`edit-hours-${taskId}`).value) || 0; | |
| const minutes = parseInt(this.shadowRoot.getElementById(`edit-minutes-${taskId}`).value) || 0; | |
| const estimatedTime = hours * 60 + minutes; | |
| this.dispatchEvent(new CustomEvent('update-task', { | |
| detail: { taskId, updates: { title, estimatedTime } }, | |
| bubbles: true | |
| })); | |
| } | |
| handleCancel(e) { | |
| const editForm = this.shadowRoot.querySelector('.edit-form'); | |
| editForm.classList.remove('active'); | |
| } | |
| handleAddTime(e) { | |
| const minutes = prompt('Wie viele Minuten willst du hinzufügen?', '15'); | |
| if (minutes && !isNaN(minutes)) { | |
| this.dispatchEvent(new CustomEvent('add-time', { | |
| detail: { | |
| taskId: this.getAttribute('task-id'), | |
| minutes: parseInt(minutes) | |
| }, | |
| bubbles: true | |
| })); | |
| } | |
| } | |
| } | |
| customElements.define('task-item', TaskItem); |