From 5035b299f1025bfe24d484082e8f4d1d7d52f48a Mon Sep 17 00:00:00 2001
From: Marco Forster <marco-forster@outlook.com>
Date: Thu, 28 Jul 2022 15:09:03 +0200
Subject: [PATCH] feat: extract kanban into components

---
 src/app/app.module.ts                         |  7 +-
 .../home/demo-board/demo-board.component.html |  1 +
 .../home/demo-board/demo-board.component.scss |  1 +
 .../home/demo-board/demo-board.component.ts   | 51 +++++++++++
 .../board-list/board-list.component.html      |  3 +
 .../board-list/board-list.component.scss      | 49 +----------
 .../kanban/board-list/board-list.component.ts | 84 +++++++++++++------
 src/app/kanban/board/board.component.scss     | 43 ----------
 src/app/kanban/board/board.component.ts       | 62 +++++++++++---
 .../kanban/dialogs/task-dialog.component.ts   |  6 +-
 .../kanban-board/kanban-board.component.html  | 19 +++++
 .../kanban-board/kanban-board.component.scss  | 23 +++++
 .../kanban-board/kanban-board.component.ts    | 62 ++++++++++++++
 src/app/kanban/kanban-routing.module.ts       |  4 +-
 src/app/kanban/kanban.module.ts               |  7 +-
 src/app/shared/shared.module.ts               |  4 +-
 16 files changed, 287 insertions(+), 139 deletions(-)
 create mode 100644 src/app/home/demo-board/demo-board.component.html
 create mode 100644 src/app/home/demo-board/demo-board.component.scss
 create mode 100644 src/app/home/demo-board/demo-board.component.ts
 create mode 100644 src/app/kanban/kanban-board/kanban-board.component.html
 create mode 100644 src/app/kanban/kanban-board/kanban-board.component.scss
 create mode 100644 src/app/kanban/kanban-board/kanban-board.component.ts

diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index cf3b7d2..edc88ea 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -11,9 +11,11 @@ import { AngularFireModule } from '@angular/fire/compat';
 import { AngularFirestoreModule } from '@angular/fire/compat/firestore';
 import { AngularFireAuthModule } from '@angular/fire/compat/auth';
 import { ErrorModule } from './error/error.module';
+import { DemoBoardComponent } from './home/demo-board/demo-board.component';
+import { KanbanModule } from './kanban/kanban.module';
 
 @NgModule({
-  declarations: [AppComponent, HomeComponent],
+  declarations: [AppComponent, HomeComponent, DemoBoardComponent],
   imports: [
     BrowserModule,
     AppRoutingModule,
@@ -22,7 +24,8 @@ import { ErrorModule } from './error/error.module';
     ErrorModule,
     AngularFireModule.initializeApp(environment.firebaseConfig),
     AngularFirestoreModule,
-    AngularFireAuthModule
+    AngularFireAuthModule,
+    KanbanModule
   ],
   providers: [],
   bootstrap: [AppComponent]
diff --git a/src/app/home/demo-board/demo-board.component.html b/src/app/home/demo-board/demo-board.component.html
new file mode 100644
index 0000000..879c911
--- /dev/null
+++ b/src/app/home/demo-board/demo-board.component.html
@@ -0,0 +1 @@
+<p>It works</p>
diff --git a/src/app/home/demo-board/demo-board.component.scss b/src/app/home/demo-board/demo-board.component.scss
new file mode 100644
index 0000000..710cecc
--- /dev/null
+++ b/src/app/home/demo-board/demo-board.component.scss
@@ -0,0 +1 @@
+/* Empty */
diff --git a/src/app/home/demo-board/demo-board.component.ts b/src/app/home/demo-board/demo-board.component.ts
new file mode 100644
index 0000000..4a1a8bb
--- /dev/null
+++ b/src/app/home/demo-board/demo-board.component.ts
@@ -0,0 +1,51 @@
+import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
+import { Component, OnInit } from '@angular/core';
+import { Board } from 'src/app/kanban/model/board.model';
+
+@Component({
+  selector: 'app-demo-board',
+  templateUrl: './demo-board.component.html',
+  styleUrls: ['./demo-board.component.scss']
+})
+export class DemoBoardComponent {
+  boards: Board[] = [
+    {
+      id: '0',
+      title: 'Todo',
+      priority: 0,
+      tasks: [
+        {
+          description: 'Plan my next awesome project',
+          label: 'yellow'
+        },
+        { description: 'Reschedule my meeting', label: 'blue' }
+      ]
+    },
+    {
+      id: '1',
+      title: 'In progress',
+      priority: 1,
+      tasks: [
+        {
+          description: 'Plan my next awesome project',
+          label: 'yellow'
+        },
+        { description: 'Reschedule my meeting', label: 'blue' }
+      ]
+    },
+    {
+      id: '2',
+      title: 'Done',
+      priority: 2,
+      tasks: [
+        {
+          description: 'Plan my next awesome project',
+          label: 'yellow'
+        },
+        { description: 'Reschedule my meeting', label: 'blue' }
+      ]
+    }
+  ];
+
+  constructor() {}
+}
diff --git a/src/app/kanban/board-list/board-list.component.html b/src/app/kanban/board-list/board-list.component.html
index 0c0a938..27c2e97 100644
--- a/src/app/kanban/board-list/board-list.component.html
+++ b/src/app/kanban/board-list/board-list.component.html
@@ -10,7 +10,10 @@
     *ngFor="let board of boards"
     [board]="board"
     (boardExitedEvent)="saveExitedBoard($event)"
+    (boardDeletedEvent)="boardDelete($event)"
     (taskDroppedEvent)="taskDrop($event)"
+    (taskUpdatedEvent)="taskUpdate($event)"
+    (taskDeletedEvent)="taskDelete($event)"
   >
     <mat-icon cdkDragHandle class="handle">drag_indicator</mat-icon>
   </app-board>
diff --git a/src/app/kanban/board-list/board-list.component.scss b/src/app/kanban/board-list/board-list.component.scss
index 456d226..1263e65 100644
--- a/src/app/kanban/board-list/board-list.component.scss
+++ b/src/app/kanban/board-list/board-list.component.scss
@@ -23,15 +23,11 @@
   }
 }
 
-.outer-card.cdk-drag-placeholder {
+.cdk-drag-placeholder {
   opacity: 0.2;
   width: 350px;
   border: 5px dashed gray;
-  margin: 0 10px;
-}
-
-.inner-card.cdk-drag-placeholder {
-  opacity: 0.5;
+  margin: 0 12px;
 }
 
 .cdk-drag-animating {
@@ -52,44 +48,3 @@
   padding: 32px;
   height: 350px;
 }
-
-.outer-card {
-  margin: 10px;
-  min-width: 300px;
-  max-width: 300px;
-  padding: 10px;
-  background: whitesmoke;
-}
-
-.inner-card {
-  margin: 5px 0;
-  cursor: pointer;
-}
-
-.blue {
-  background: #71deff;
-  color: black;
-}
-
-.green {
-  background: #36e9b6;
-  color: black;
-}
-
-.yellow {
-  background: #ffcf44;
-  color: black;
-}
-
-.purple {
-  background: #b15cff;
-}
-
-.red {
-  background: #e74a4a;
-}
-
-.gray {
-  background: gray;
-  text-decoration: line-through;
-}
diff --git a/src/app/kanban/board-list/board-list.component.ts b/src/app/kanban/board-list/board-list.component.ts
index ef41aca..cff3e0d 100644
--- a/src/app/kanban/board-list/board-list.component.ts
+++ b/src/app/kanban/board-list/board-list.component.ts
@@ -3,41 +3,58 @@ import {
   moveItemInArray,
   transferArrayItem
 } from '@angular/cdk/drag-drop';
-import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
 import { Subscription } from 'rxjs';
 import { Board } from '../model/board.model';
 import { BoardService } from '../board.service';
 import { MatDialog } from '@angular/material/dialog';
 import { BoardDialogComponent } from '../dialogs/board-dialog.component';
+import { Task } from '../model/task.model';
 
 @Component({
   selector: 'app-board-list',
   templateUrl: './board-list.component.html',
   styleUrls: ['./board-list.component.scss']
 })
-export class BoardListComponent implements OnInit, OnDestroy {
-  boards: Board[] = [];
+export class BoardListComponent {
+  @Output()
+  boardDroppedEvent = new EventEmitter<Board[]>();
+
+  @Output()
+  boardCreatedEvent = new EventEmitter<{ title: string; priority: number }>();
+
+  @Output()
+  boardDeletedEvent = new EventEmitter<string>();
+
+  @Output()
+  taskDroppedEvent = new EventEmitter<
+    Array<{
+      boardId: string;
+      tasks: Task[];
+    }>
+  >();
+
+  @Output()
+  taskUpdatedEvent = new EventEmitter<{
+    boardId: string;
+    tasks: Task[];
+  }>();
+
+  @Output()
+  taskDeletedEvent = new EventEmitter<{ boardId: string; task: Task }>();
+
+  @Input()
+  boards?: Board[];
+
   previousBoard: Board = {};
   dropped = true;
   subscription?: Subscription;
 
   constructor(public boardService: BoardService, public dialog: MatDialog) {}
 
-  ngOnInit() {
-    this.subscription = this.boardService
-      .getUserBoards()
-      .subscribe((boards) => {
-        this.boards = boards;
-      });
-  }
-
-  ngOnDestroy() {
-    this.subscription?.unsubscribe();
-  }
-
   boardDrop(event: CdkDragDrop<string[]>) {
-    moveItemInArray(this.boards, event.previousIndex, event.currentIndex);
-    this.boardService.sortBoards(this.boards);
+    moveItemInArray(this.boards!, event.previousIndex, event.currentIndex);
+    this.boardDroppedEvent.emit(this.boards);
   }
 
   openBoardDialog(): void {
@@ -48,9 +65,10 @@ export class BoardListComponent implements OnInit, OnDestroy {
 
     dialogRef.afterClosed().subscribe((result) => {
       if (result) {
-        this.boardService.createBoard({
+        this.boards?.push({ title: result, priority: this.boards?.length! });
+        this.boardCreatedEvent.emit({
           title: result,
-          priority: this.boards.length
+          priority: this.boards?.length!
         });
       }
     });
@@ -76,7 +94,9 @@ export class BoardListComponent implements OnInit, OnDestroy {
         event.previousIndex,
         event.currentIndex
       );
-      this.boardService.updateTasks(currentBoard.id!, currentBoard.tasks!);
+      this.taskDroppedEvent.emit([
+        { boardId: currentBoard.id!, tasks: currentBoard.tasks! }
+      ]);
     } else {
       transferArrayItem(
         this.previousBoard.tasks!,
@@ -84,12 +104,26 @@ export class BoardListComponent implements OnInit, OnDestroy {
         event.previousIndex,
         event.currentIndex
       );
-      this.boardService.updateTasks(
-        this.previousBoard.id!,
-        this.previousBoard.tasks!
-      );
-      this.boardService.updateTasks(currentBoard.id!, currentBoard.tasks!);
+      this.taskDroppedEvent.emit([
+        { boardId: currentBoard.id!, tasks: currentBoard.tasks! },
+        { boardId: this.previousBoard.id!, tasks: this.previousBoard.tasks! }
+      ]);
     }
     this.dropped = true;
   }
+
+  taskUpdate({ boardId, tasks }: { boardId: string; tasks: Task[] }) {
+    this.taskUpdatedEvent.emit({ boardId: boardId, tasks: tasks });
+  }
+
+  boardDelete(boardId: string) {
+    this.boards?.forEach((board, idx) => {
+      if (board.id === boardId) this.boards?.splice(idx, 1);
+    });
+    this.boardDeletedEvent.emit(boardId);
+  }
+
+  taskDelete({ boardId, task }: { boardId: string; task: Task }) {
+    this.taskDeletedEvent.emit({ boardId: boardId, task: task });
+  }
 }
diff --git a/src/app/kanban/board/board.component.scss b/src/app/kanban/board/board.component.scss
index 456d226..0056fc9 100644
--- a/src/app/kanban/board/board.component.scss
+++ b/src/app/kanban/board/board.component.scss
@@ -1,35 +1,3 @@
-.boards {
-  width: auto;
-  padding: 24px;
-  display: flex;
-  flex-direction: row;
-  overflow-x: scroll;
-
-  &::-webkit-scrollbar {
-    height: 4px;
-    width: 4px;
-  }
-
-  &::-webkit-scrollbar-thumb {
-    background-color: #f5f5f5;
-    border: 2px solid #555;
-  }
-
-  .handle {
-    position: relative;
-    top: 5px;
-    left: 0;
-    cursor: move;
-  }
-}
-
-.outer-card.cdk-drag-placeholder {
-  opacity: 0.2;
-  width: 350px;
-  border: 5px dashed gray;
-  margin: 0 10px;
-}
-
 .inner-card.cdk-drag-placeholder {
   opacity: 0.5;
 }
@@ -42,17 +10,6 @@
   transition: transform 300ms ease;
 }
 
-.board-button {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  border: 5px gray dashed;
-  width: 300px;
-  padding: 32px;
-  height: 350px;
-}
-
 .outer-card {
   margin: 10px;
   min-width: 300px;
diff --git a/src/app/kanban/board/board.component.ts b/src/app/kanban/board/board.component.ts
index d3e916e..366fa24 100644
--- a/src/app/kanban/board/board.component.ts
+++ b/src/app/kanban/board/board.component.ts
@@ -12,14 +12,29 @@ import { Task } from '../model/task.model';
   styleUrls: ['./board.component.scss']
 })
 export class BoardComponent {
-  @Output() boardExitedEvent = new EventEmitter<Board>();
+  @Output()
+  boardExitedEvent = new EventEmitter<Board>();
 
-  @Output() taskDroppedEvent = new EventEmitter<{
+  @Output()
+  boardDeletedEvent = new EventEmitter<string>();
+
+  @Output()
+  taskDroppedEvent = new EventEmitter<{
     event: CdkDragDrop<string[]>;
     currentBoard: Board;
   }>();
 
-  @Input() board: Board = {};
+  @Output()
+  taskUpdatedEvent = new EventEmitter<{
+    boardId: string;
+    tasks: Task[];
+  }>();
+
+  @Output()
+  taskDeletedEvent = new EventEmitter<{ boardId: string; task: Task }>();
+
+  @Input()
+  board: Board = {};
 
   constructor(public boardService: BoardService, public dialog: MatDialog) {}
 
@@ -36,26 +51,47 @@ export class BoardComponent {
     const dialogRef = this.dialog.open(TaskDialogComponent, {
       width: '500px',
       data: task
-        ? { task: { ...task }, isNew: false, boardId: board?.id, idx }
-        : { task: newTask, isNew: true, boardId: board?.id }
+        ? {
+            task: { ...task },
+            isNew: false,
+            toDelete: false,
+            boardId: board?.id,
+            idx
+          }
+        : { task: newTask, isNew: true, toDelete: false, boardId: board?.id }
     });
 
     dialogRef.afterClosed().subscribe((result) => {
       if (result) {
-        if (result.isNew) {
-          this.boardService.updateTasks(board.id!, [
-            ...board.tasks!,
-            result.task
-          ]);
+        if (result.toDelete) {
+          board.tasks?.forEach((task, idx) => {
+            if (
+              task.description === result.task.description &&
+              task.label === result.task.label
+            )
+              this.board?.tasks?.splice(idx, 1);
+          });
+          this.taskDeletedEvent.emit({ boardId: board.id!, task: result.task });
         } else {
-          board.tasks?.splice(result.idx, 1, result.task);
-          this.boardService.updateTasks(board.id!, board.tasks!);
+          if (result.isNew) {
+            board.tasks?.push(result.task);
+            this.taskUpdatedEvent.emit({
+              boardId: board.id!,
+              tasks: board.tasks!
+            });
+          } else {
+            board.tasks?.splice(result.idx, 1, result.task);
+            this.taskUpdatedEvent.emit({
+              boardId: board.id!,
+              tasks: board.tasks!
+            });
+          }
         }
       }
     });
   }
 
   handleBoardDelete(board: Board) {
-    this.boardService.deleteBoard(board.id!);
+    this.boardDeletedEvent.emit(board.id);
   }
 }
diff --git a/src/app/kanban/dialogs/task-dialog.component.ts b/src/app/kanban/dialogs/task-dialog.component.ts
index 01cc345..a54bfb6 100644
--- a/src/app/kanban/dialogs/task-dialog.component.ts
+++ b/src/app/kanban/dialogs/task-dialog.component.ts
@@ -1,6 +1,5 @@
 import { Component, Inject } from '@angular/core';
 import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
-import { BoardService } from '../board.service';
 
 @Component({
   selector: 'app-task-dialog',
@@ -43,7 +42,6 @@ export class TaskDialogComponent {
 
   constructor(
     public dialogRef: MatDialogRef<TaskDialogComponent>,
-    private boardService: BoardService,
     @Inject(MAT_DIALOG_DATA) public data: any
   ) {}
 
@@ -52,7 +50,7 @@ export class TaskDialogComponent {
   }
 
   handleTaskDelete() {
-    this.boardService.removeTask(this.data.boardId, this.data.task);
-    this.dialogRef.close();
+    this.data.toDelete = true;
+    this.dialogRef.close(this.data);
   }
 }
diff --git a/src/app/kanban/kanban-board/kanban-board.component.html b/src/app/kanban/kanban-board/kanban-board.component.html
new file mode 100644
index 0000000..6859297
--- /dev/null
+++ b/src/app/kanban/kanban-board/kanban-board.component.html
@@ -0,0 +1,19 @@
+<div class="container">
+  <div class="header" *ngIf="angularFireAuth.user | async as user">
+    <img class="avatar" [src]="user.photoURL" />
+    <div>
+      <h1 class="title">Kanban Board of {{ user.displayName }}</h1>
+      <h2 class="subtitle">{{ user.email }}</h2>
+    </div>
+  </div>
+
+  <app-board-list
+    [boards]="boards"
+    (boardDroppedEvent)="sortBoards($event)"
+    (boardCreatedEvent)="createBoard($event)"
+    (boardDeletedEvent)="deleteBoard($event)"
+    (taskDroppedEvent)="sortTasks($event)"
+    (taskUpdatedEvent)="updateTasks($event)"
+    (taskDeletedEvent)="removeTask($event)"
+  ></app-board-list>
+</div>
diff --git a/src/app/kanban/kanban-board/kanban-board.component.scss b/src/app/kanban/kanban-board/kanban-board.component.scss
new file mode 100644
index 0000000..94a3710
--- /dev/null
+++ b/src/app/kanban/kanban-board/kanban-board.component.scss
@@ -0,0 +1,23 @@
+.container {
+  margin: 30px;
+}
+
+.header {
+  display: flex;
+}
+
+.avatar {
+  margin-right: 10px;
+  border-radius: 50%;
+  height: min-content;
+}
+
+.title {
+  margin-bottom: 0;
+}
+
+.subtitle {
+  margin-top: 0;
+  font-size: medium;
+  color: gray;
+}
diff --git a/src/app/kanban/kanban-board/kanban-board.component.ts b/src/app/kanban/kanban-board/kanban-board.component.ts
new file mode 100644
index 0000000..abd9d02
--- /dev/null
+++ b/src/app/kanban/kanban-board/kanban-board.component.ts
@@ -0,0 +1,62 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { AngularFireAuth } from '@angular/fire/compat/auth';
+import { Subscription } from 'rxjs';
+import { BoardService } from '../board.service';
+import { Board } from '../model/board.model';
+import { Task } from '../model/task.model';
+
+@Component({
+  selector: 'app-kanban-board',
+  templateUrl: './kanban-board.component.html',
+  styleUrls: ['./kanban-board.component.scss']
+})
+export class KanbanBoardComponent implements OnInit, OnDestroy {
+  boards?: Board[];
+  subscription?: Subscription;
+
+  constructor(
+    private boardService: BoardService,
+    public angularFireAuth: AngularFireAuth
+  ) {}
+
+  ngOnInit(): void {
+    this.subscription = this.boardService
+      .getUserBoards()
+      .subscribe((boards) => {
+        this.boards = boards;
+      });
+  }
+
+  ngOnDestroy() {
+    this.subscription?.unsubscribe();
+  }
+
+  sortBoards(boards: Board[]) {
+    this.boardService.sortBoards(boards);
+  }
+
+  createBoard({ title, priority }: { title: string; priority: number }) {
+    this.boardService.createBoard({
+      title: title,
+      priority: priority
+    });
+  }
+
+  deleteBoard(boardId: string) {
+    this.boardService.deleteBoard(boardId);
+  }
+
+  sortTasks(boardsToSort: { boardId: string; tasks: Task[] }[]) {
+    boardsToSort.forEach((board) => {
+      this.boardService.updateTasks(board.boardId, board.tasks);
+    });
+  }
+
+  updateTasks({ boardId, tasks }: { boardId: string; tasks: Task[] }) {
+    this.boardService.updateTasks(boardId, tasks);
+  }
+
+  removeTask({ boardId, task }: { boardId: string; task: Task }) {
+    this.boardService.removeTask(boardId, task);
+  }
+}
diff --git a/src/app/kanban/kanban-routing.module.ts b/src/app/kanban/kanban-routing.module.ts
index b3debe1..1e1054c 100644
--- a/src/app/kanban/kanban-routing.module.ts
+++ b/src/app/kanban/kanban-routing.module.ts
@@ -1,8 +1,8 @@
 import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
-import { BoardListComponent } from './board-list/board-list.component';
+import { KanbanBoardComponent } from './kanban-board/kanban-board.component';
 
-const routes: Routes = [{ path: '', component: BoardListComponent }];
+const routes: Routes = [{ path: '', component: KanbanBoardComponent }];
 
 @NgModule({
   imports: [RouterModule.forChild(routes)],
diff --git a/src/app/kanban/kanban.module.ts b/src/app/kanban/kanban.module.ts
index 038a2e9..0902ead 100644
--- a/src/app/kanban/kanban.module.ts
+++ b/src/app/kanban/kanban.module.ts
@@ -9,13 +9,15 @@ import { BoardListComponent } from './board-list/board-list.component';
 import { BoardDialogComponent } from './dialogs/board-dialog.component';
 import { TaskDialogComponent } from './dialogs/task-dialog.component';
 import { BoardComponent } from './board/board.component';
+import { KanbanBoardComponent } from './kanban-board/kanban-board.component';
 
 @NgModule({
   declarations: [
     BoardListComponent,
     BoardDialogComponent,
     TaskDialogComponent,
-    BoardComponent
+    BoardComponent,
+    KanbanBoardComponent
   ],
   imports: [
     CommonModule,
@@ -23,6 +25,7 @@ import { BoardComponent } from './board/board.component';
     SharedModule,
     FormsModule,
     DragDropModule
-  ]
+  ],
+  exports: [BoardComponent]
 })
 export class KanbanModule {}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 583f7bb..fbcdd0f 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -14,6 +14,7 @@ import { MatDialogModule } from '@angular/material/dialog';
 import { MatFormFieldModule } from '@angular/material/form-field';
 import { MatInputModule } from '@angular/material/input';
 import { MatSnackBarModule } from '@angular/material/snack-bar';
+import { MatGridListModule } from '@angular/material/grid-list';
 import { ToolbarComponent } from './toolbar/toolbar.component';
 import {
   FontAwesomeModule,
@@ -46,7 +47,8 @@ const modules = [
   MatDialogModule,
   MatButtonToggleModule,
   RouterModule,
-  FontAwesomeModule
+  FontAwesomeModule,
+  MatGridListModule
 ];
 
 @NgModule({