tehnicni-app/client/libs/web/tasks/src/lib/task-card.component.ts
Gal Podlipnik fd276b2c07 theme
2025-07-14 20:40:45 +02:00

262 lines
6.6 KiB
TypeScript

import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
effect,
input,
output,
signal,
} from '@angular/core';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { CommentResponse, TaskResponse, User } from '@org/shared/api';
import { TaskCommentComponent } from './task-comment.component';
@Component({
selector: 'lib-task-card',
imports: [
CommonModule,
MatExpansionModule,
MatIconModule,
MatDividerModule,
MatBadgeModule,
MatButtonModule,
TaskCommentComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<mat-expansion-panel class="task-panel">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="task-title-container">
<mat-icon>assignment</mat-icon>
<span>{{ task().tasks.title }}</span>
</div>
</mat-panel-title>
<mat-panel-description>
<div class="task-info">
<div class="task-info-row">
<span class="task-author"
>By {{ getUserName(task().tasks.userId) }}</span
>
@if (taskComments().length) {
<mat-icon
class="comment-badge"
matBadge="{{ taskComments().length }}"
matBadgeColor="accent"
>
comment
</mat-icon>
}
</div>
<span class="task-timestamp">{{
formatDate(task().tasks.timestamp)
}}</span>
</div>
</mat-panel-description>
</mat-expansion-panel-header>
<div class="task-details">
<p class="task-description">
{{ task().tasks.description }}
</p>
<mat-divider></mat-divider>
<div class="comments-section">
<h3>
<mat-icon>forum</mat-icon>
Comments
</h3>
<lib-task-comment
[taskId]="task().tasks.id"
[comments]="taskComments()"
[users]="users()"
[submitting]="addingComment()"
(addComment)="handleAddComment($event)"
></lib-task-comment>
</div>
</div>
</mat-expansion-panel>
`,
styles: [
`
.task-panel {
margin-bottom: 1rem;
border-radius: var(--mat-sys-corner-small);
box-shadow: var(--mat-sys-level1);
transition: all 0.3s ease;
}
.task-panel:hover {
box-shadow: var(--mat-sys-level3);
}
.task-panel .mat-expansion-panel-header {
height: auto;
min-height: 64px;
padding: 0.75rem 1rem;
}
.task-title-container {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
min-width: 0;
margin-right: 1rem;
}
.task-title-container mat-icon {
color: #1976d2;
}
.task-title-container span {
font-weight: 500;
font-size: 1.1rem;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}
.task-info {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
min-width: 150px;
text-align: right;
}
.task-info-row {
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 0.5rem;
}
.task-author {
font-weight: 500;
color: #424242;
}
.task-timestamp {
font-size: 0.8rem;
color: var(--mat-sys-on-surface-variant);
}
.task-details {
padding: 1rem 0;
}
.task-description {
font-size: var(--mat-sys-body-large-size);
line-height: 1.6;
margin-bottom: 1.5rem;
white-space: pre-line;
color: var(--mat-sys-on-surface);
background-color: var(--mat-sys-surface-container-low);
padding: 1rem;
border-radius: var(--mat-sys-corner-small);
border-left: 4px solid var(--mat-sys-primary);
}
.comments-section {
margin-top: 1.5rem;
}
.comments-section h3 {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: var(--mat-sys-title-medium-size);
margin-bottom: 1rem;
color: var(--mat-sys-on-surface);
}
.comments-section h3 mat-icon {
color: #1976d2;
}
@media (max-width: 768px) {
.task-info {
align-items: flex-start;
}
}
`,
],
})
export class TaskCardComponent {
readonly task = input.required<TaskResponse>();
readonly users = input<User[]>([]);
readonly comments = input<CommentResponse[]>([]);
readonly addingComment = input<boolean>(false);
readonly commentAdded = output<{
taskId: number;
content: string;
userId: number;
}>();
readonly taskComments = signal<CommentResponse[]>([]);
constructor() {
// Update task comments whenever comments or task changes
effect(() => {
const taskId = this.task()?.tasks?.id;
if (taskId && this.comments()) {
this.taskComments.set(
this.comments().filter(
(comment) => comment.comments.taskId === taskId
)
);
}
});
}
getUserName(userId: number): string {
const user = this.users().find((u) => u.id === userId);
return user ? user.name : 'Unknown User';
}
formatDate(timestamp?: string): string {
if (!timestamp) return 'Unknown date';
const date = new Date(timestamp);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return (
'Today at ' +
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
);
} else if (diffDays === 1) {
return (
'Yesterday at ' +
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
);
} else if (diffDays < 7) {
return `${diffDays} days ago`;
} else {
return date.toLocaleDateString([], {
day: 'numeric',
month: 'short',
year: 'numeric',
});
}
}
handleAddComment(commentData: { content: string; userId: number }) {
this.commentAdded.emit({
taskId: this.task().tasks.id,
...commentData,
});
}
}