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