refactor: optimize and add comments to TaskComponent

This commit is contained in:
Chneemann 2025-04-15 23:17:45 +02:00
parent b75fb18e42
commit c02cf4d7a3
3 changed files with 182 additions and 137 deletions

View file

@ -1,8 +1,9 @@
<section> <section>
@if (task.id) { <!-- If the task exists, display it -->
@if (task && task.id) {
<div (click)="handleMenuButtonClick($event, task.id)"> <div (click)="handleMenuButtonClick($event, task.id)">
<div <div
class="content" class="task"
[draggable]="disableDrag" [draggable]="disableDrag"
(dragstart)=" (dragstart)="
disableDrag ? dragDropService.startDragging($event, task) : null disableDrag ? dragDropService.startDragging($event, task) : null
@ -13,8 +14,10 @@
class="category" class="category"
[style.background-color]="categoryColors.get(task.category)" [style.background-color]="categoryColors.get(task.category)"
> >
{{ task.category }} <p>{{ task.category }}</p>
</div> </div>
<!-- If the user is on a mobile device, show the menu button -->
@if (pageViewMedia$ | async) { @if (pageViewMedia$ | async) {
<div class="menu-btn" (click)="handleMenuButtonClick($event, task.id)"> <div class="menu-btn" (click)="handleMenuButtonClick($event, task.id)">
<img <img
@ -25,10 +28,13 @@
</div> </div>
} }
</div> </div>
<div class="headline">{{ task.title }}</div>
<div class="title">{{ task.title }}</div>
<div class="description"> <div class="description">
{{ task.description }} {{ task.description }}
</div> </div>
<!-- If the task has subtasks, display them -->
@if(task.subtasks.length > 0) { @if(task.subtasks.length > 0) {
<div class="subtask"> <div class="subtask">
<div class="subtask-line"> <div class="subtask-line">
@ -37,46 +43,67 @@
[style.width.%]="completedSubtasksPercent()" [style.width.%]="completedSubtasksPercent()"
></span> ></span>
</div> </div>
<div class="subtask-text"> <p>{{ completedSubtasks() }} / {{ task.subtasks.length }} Subtasks</p>
{{ completedSubtasks() }} / {{ task.subtasks.length }} Subtasks
</div>
</div> </div>
} }
<div class="footer">
<div class="footer-badge"> <div class="assignees">
@for (user of task.userData; track user) { @if (task.creator === <div class="assigned">
user.id) { <!-- Loop through user data -->
<span @for (user of task.userData; track user) {
class="footer-badged"
<!-- If this user is the creator of the task, display the badge -->
@if (task.creator === user.id) {
<div
class="assigned-badged"
(mousemove)="openDialog(task.creator, $event)" (mousemove)="openDialog(task.creator, $event)"
(mouseleave)="closeDialog()" (mouseleave)="closeDialog()"
[style.background-color]="user.color" [style.background-color]="user.color"
>{{ user.initials }}</span >
>} } @for (user of task.userData; track user) { @for (assigned of <span>{{ user.initials }}</span>
task.assignees; track assigned) { @if (assigned.userId === user.id) { </div>
<span } }
class="footer-badged"
<!-- Loop through user data -->
@for (user of task.userData; track user) {
<!-- Loop through task assignees -->
@for (assigned of task.assignees; track assigned) {
<!-- If this user is assigned to the task, display the badge -->
@if (assigned.userId === user.id) {
<div
class="assigned-badged"
(mousemove)="openDialog(user.id, $event)" (mousemove)="openDialog(user.id, $event)"
(mouseleave)="closeDialog()" (mouseleave)="closeDialog()"
[style.background-color]="user.color" [style.background-color]="user.color"
>{{ user.initials }}</span
> >
<span>{{ user.initials }}</span>
</div>
} } } } } }
</div> </div>
<div class="footer-priority prio-{{ task.priority }}"></div> <div class="priority priority-{{ task.priority }}"></div>
</div> </div>
</div> </div>
</div> </div>
@if (menuOpen){
<!-- If the task menu is open, display it -->
@if (mobileMenuOpen){
<app-task-menu <app-task-menu
[task]="task" [task]="task"
[boardTaskStatus]="task.status" [boardTaskStatus]="task.status"
(updateStatusEmitter)="onStatusUpdate($event)" (updateStatusEmitter)="onStatusUpdate($event)"
></app-task-menu> ></app-task-menu>
} @if (AssignedDialogId) { }
<!-- If the assigned dialog is open, display it -->
@if (assignedDialogId) {
<div class="dialog" [style.left.px]="dialogX" [style.top.px]="dialogY"> <div class="dialog" [style.left.px]="dialogX" [style.top.px]="dialogY">
@for (user of task.userData; track user) { @if (user.id === <!-- Loop through user data -->
AssignedDialogId) { @for (user of task.userData; track user) {
<!-- If the user id matches the assigned dialog id, display the user details -->
@if (user.id === assignedDialogId) {
<p> <p>
{{ user.firstName }} {{ user.firstName }}
</p> </p>

View file

@ -4,8 +4,7 @@ section {
position: relative; position: relative;
} }
.content { .task {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@ -13,27 +12,12 @@ section {
padding: 16px; padding: 16px;
border-radius: 24px; border-radius: 24px;
background-color: var(--white); background-color: var(--white);
box-shadow: 0px 0px 10px 3px rgba(0, 0, 0, 0.08); box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.08);
margin-bottom: 16px; margin-bottom: 16px;
cursor: pointer; cursor: pointer;
&:hover {
box-shadow: 0px 0px 10px 3px rgba(0, 0, 0, 0.3);
}
}
.dialog { &:hover {
position: fixed; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
width: fit-content;
height: fit-content;
padding: 10px 15px;
border-radius: 0px 20px 20px 20px;
border: 1px solid var(--black);
z-index: 2;
background-color: var(--white);
p,
span {
font-size: 18px;
font-weight: 400;
} }
} }
@ -41,23 +25,31 @@ section {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
.category { .category {
width: fit-content; width: fit-content;
font-size: 16px;
padding: 4px 16px; padding: 4px 16px;
border-radius: 8px; border-radius: 8px;
color: var(--white); color: var(--white);
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.2);
p {
font-size: 16px;
text-shadow: 1px 1px 2px var(--black); text-shadow: 1px 1px 2px var(--black);
} }
}
.menu-btn { .menu-btn {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 6px 12px; padding: 6px 12px;
img { img {
height: 16px; height: 16px;
width: auto; width: auto;
} }
&:hover { &:hover {
img { img {
filter: brightness(0) saturate(100%) invert(56%) sepia(64%) filter: brightness(0) saturate(100%) invert(56%) sepia(64%)
@ -67,9 +59,9 @@ section {
} }
} }
.headline { .title {
color: var(--dark-blue); color: var(--dark-blue);
font-size: 16px; font-size: 18px;
font-weight: 700; font-weight: 700;
margin-top: 12px; margin-top: 12px;
} }
@ -83,11 +75,18 @@ section {
overflow: auto; overflow: auto;
} }
// Subtasks
.subtask { .subtask {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-top: 24px; margin-top: 24px;
p {
font-size: 12px;
font-weight: 400;
}
} }
.subtask-line { .subtask-line {
@ -100,78 +99,101 @@ section {
align-items: flex-start; align-items: flex-start;
border-radius: 8px; border-radius: 8px;
background-color: var(--very-light-gray); background-color: var(--very-light-gray);
}
.filler-full { .filler-full {
border-radius: 16px; border-radius: 16px;
background-color: var(--light-blue); background-color: var(--light-blue);
height: 8px; height: 8px;
}
} }
.subtask-text { // Assignees
font-size: 12px;
font-weight: 400;
}
.footer { .assignees {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-top: 24px; margin-top: 15px;
width: 100%; width: 100%;
}
.footer-badge { .assigned {
display: flex; display: flex;
align-items: center;
position: relative;
width: 170px; width: 170px;
height: 45px;
gap: 6px;
overflow: auto; overflow: auto;
}
} }
.footer-badged { .assigned-badged {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-width: 32px; width: 32px;
min-height: 32px; height: 32px;
border-radius: 45px; border-radius: 100%;
border: 1px solid var(--white); border: 1px solid var(--black);
position: relative;
transition: box-shadow 0.2s ease, transform 0.2s ease;
margin-left: 3px;
&:nth-child(n + 2) {
margin-left: -12px;
}
& span {
color: var(--white); color: var(--white);
font-size: 12px; font-size: 14px;
font-weight: 400; font-weight: 700;
}
.footer-badge span {
margin-right: 4px;
text-shadow: 1px 1px 2px var(--black); text-shadow: 1px 1px 2px var(--black);
}
&:hover {
transform: translateY(-1px) scale(1.02);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 2;
}
} }
.footer-badge span:first-child { // Priority
margin-left: 0 !important;
}
.footer-priority { .priority {
background-size: 32px; background-size: 32px;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
width: 32px; width: 32px;
height: 32px; height: 32px;
} }
.priority {
background-size: 24px;
background-repeat: no-repeat;
background-position: center;
width: 24px;
height: 24px;
}
.prio-urgent { .priority-urgent {
background-image: url(./../../../../assets/img/urgent.svg); background-image: url(./../../../../assets/img/urgent.svg);
} }
.prio-medium { .priority-medium {
background-image: url(./../../../../assets/img/medium.svg); background-image: url(./../../../../assets/img/medium.svg);
} }
.prio-low { .priority-low {
background-image: url(./../../../../assets/img/low.svg); background-image: url(./../../../../assets/img/low.svg);
} }
// Dialog
.dialog {
position: fixed;
width: fit-content;
height: fit-content;
padding: 10px 15px;
border-radius: 0px 20px 20px 20px;
border: 1px solid var(--black);
z-index: 3;
background-color: var(--white);
p,
span {
font-size: 18px;
font-weight: 400;
}
}

View file

@ -33,22 +33,20 @@ export class TaskComponent {
readonly DIALOG_OFFSET_X = 25; readonly DIALOG_OFFSET_X = 25;
readonly DIALOG_OFFSET_Y = 10; readonly DIALOG_OFFSET_Y = 10;
pageViewMedia$ = this.resizeService.pageViewMedia$;
assignees: Assignee[] = [];
categoryColors = new Map<string, string>([ categoryColors = new Map<string, string>([
['User Story', '#0038ff'], ['User Story', '#0038ff'],
['Technical Task', '#20d7c2'], ['Technical Task', '#20d7c2'],
]); ]);
pageViewMedia$ = this.resizeService.pageViewMedia$; mobileMenuOpen = false;
menuOpen = false;
disableDrag = false; disableDrag = false;
AssignedDialogId = ''; assignedDialogId = '';
dialogX = 0; dialogX = 0;
dialogY = 0; dialogY = 0;
creator = '';
assignees: Assignee[] = [];
constructor( constructor(
public dragDropService: DragDropService, public dragDropService: DragDropService,
public overlayService: OverlayService, public overlayService: OverlayService,
@ -78,20 +76,20 @@ export class TaskComponent {
* If the event target is a menu button or menu img, toggle the task menu * If the event target is a menu button or menu img, toggle the task menu
* If the event target is anything else, open the task details overlay * If the event target is anything else, open the task details overlay
*/ */
handleMenuButtonClick(event: MouseEvent, taskId: string) { handleMenuButtonClick(event: MouseEvent, taskId: string): void {
event.stopPropagation(); event.stopPropagation();
const targetElement = event.target as HTMLElement; const targetElement = event.target as HTMLElement;
targetElement.classList.contains('menu-btn') || targetElement.classList.contains('menu-btn') ||
targetElement.classList.contains('menu-img') targetElement.classList.contains('menu-img')
? this.toggleTaskMenu() ? this.toggleMobileMenu()
: this.openTaskDetailsOverlay(taskId); : this.openTaskDetailsOverlay(taskId);
} }
/** /**
* Toggle the task menu for the current task * Toggle the task menu for the current task
*/ */
toggleTaskMenu() { toggleMobileMenu(): void {
this.menuOpen = !this.menuOpen; this.mobileMenuOpen = !this.mobileMenuOpen;
} }
/** /**
@ -102,9 +100,9 @@ export class TaskComponent {
* If the page is in the media view, navigate to the task details page * If the page is in the media view, navigate to the task details page
* If the page is not in the media view, open the task overlay * If the page is not in the media view, open the task overlay
*/ */
openTaskDetailsOverlay(taskId: string) { openTaskDetailsOverlay(taskId: string): void {
if (this.menuOpen) { if (this.mobileMenuOpen) {
this.toggleTaskMenu(); this.toggleMobileMenu();
} }
this.pageViewMedia$.pipe(take(1)).subscribe((isPageViewMedia) => { this.pageViewMedia$.pipe(take(1)).subscribe((isPageViewMedia) => {
isPageViewMedia isPageViewMedia
@ -113,33 +111,13 @@ export class TaskComponent {
}); });
} }
@HostListener('document:click', ['$event'])
/**
* If the target element of the event is not a task menu button,
* task menu img, or the task menu itself, close the task menu
* @param event the MouseEvent
*/
checkToggleTaskMenu(event: MouseEvent) {
const targetElement = event.target as HTMLElement;
const menuSelectors = ['.menu-btn', '.menu-img', 'app-task-menu'];
const isMenuClicked = menuSelectors.some((selector) =>
targetElement.closest(selector)
);
if (!isMenuClicked) {
this.menuOpen = false;
}
}
// User Dialog
/** /**
* Opens the user dialog for the given user id at the position of the mouse click * Opens the user dialog for the given user id at the position of the mouse click
* @param userId the id of the user * @param userId the id of the user
* @param event the MouseEvent that triggered the dialog * @param event the MouseEvent that triggered the dialog
*/ */
openDialog(userId: any, event: MouseEvent) { openDialog(userId: string, event: MouseEvent): void {
this.AssignedDialogId = userId; this.assignedDialogId = userId;
this.updateDialogPosition(event); this.updateDialogPosition(event);
} }
@ -147,7 +125,7 @@ export class TaskComponent {
* Updates the position of the user dialog based on the MouseEvent * Updates the position of the user dialog based on the MouseEvent
* @param event the MouseEvent that triggered the dialog * @param event the MouseEvent that triggered the dialog
*/ */
updateDialogPosition(event: MouseEvent) { updateDialogPosition(event: MouseEvent): void {
this.dialogX = event.clientX + this.DIALOG_OFFSET_X; this.dialogX = event.clientX + this.DIALOG_OFFSET_X;
this.dialogY = event.clientY + this.DIALOG_OFFSET_Y; this.dialogY = event.clientY + this.DIALOG_OFFSET_Y;
} }
@ -155,12 +133,10 @@ export class TaskComponent {
/** /**
* Closes the user dialog * Closes the user dialog
*/ */
closeDialog() { closeDialog(): void {
this.AssignedDialogId = ''; this.assignedDialogId = '';
} }
// Subtasks
/** /**
* Returns the number of completed subtasks of the task * Returns the number of completed subtasks of the task
* @returns the number of completed subtasks * @returns the number of completed subtasks
@ -178,26 +154,46 @@ export class TaskComponent {
(subtask) => subtask.status (subtask) => subtask.status
).length; ).length;
return (completedSubtasksCount / this.task.subtasks.length) * 100; return this.task.subtasks.length > 0
? (completedSubtasksCount / this.task.subtasks.length) * 100
: 0;
} }
/** /**
* Emits an event to update the status of a task. * Emits an event to update the status of a task.
* @param event The TaskMoveEvent containing the task and the new status to move to. * @param event The TaskMoveEvent containing the task and the new status to move to.
*/ */
onStatusUpdate(event: TaskMoveEvent) { onStatusUpdate(event: TaskMoveEvent): void {
this.updateStatusEmitter.emit({ this.updateStatusEmitter.emit({
task: event.task, task: event.task,
moveTo: event.moveTo, moveTo: event.moveTo,
}); });
} }
@HostListener('document:click', ['$event'])
/**
* If the target element of the event is not a task menu button,
* task menu img, or the task menu itself, close the task menu
* @param event the MouseEvent
*/
checkToggleTaskMenu(event: MouseEvent): void {
const targetElement = event.target as HTMLElement;
const menuSelectors = ['.menu-btn', '.menu-img', 'app-task-menu'];
const isMenuClicked = menuSelectors.some((selector) =>
targetElement.closest(selector)
);
if (!isMenuClicked) {
this.mobileMenuOpen = false;
}
}
@HostListener('window:resize') @HostListener('window:resize')
/** /**
* Called when the window is resized. * Called when the window is resized.
* Updates the disableDragStatus so that tasks are not draggable on mobile devices. * Updates the disableDragStatus so that tasks are not draggable on mobile devices.
*/ */
onResize() { onResize(): void {
this.updateDisableDragStatus(); this.updateDisableDragStatus();
} }
} }