From ad272300ebfb50608f9bff1c94f2cf65e2d5d884 Mon Sep 17 00:00:00 2001 From: Rayan Konecny do Nascimento Date: Thu, 17 Apr 2025 01:55:58 +0000 Subject: [PATCH] Add toggle messages --- angular.json | 2 +- db.json | 29 +++- package-lock.json | 17 ++ package.json | 1 + src/app/app.config.ts | 4 +- .../category/constants/category-colors.ts | 12 +- .../category/services/category.service.ts | 4 +- .../include-task-form.component.ts | 150 ++++++++++++++++++ .../inclusion-form.component.ts | 42 +++++ .../task/constants/create-task-form.ts | 27 ++++ .../task/service/task.service.spec.ts | 11 +- src/app/features/task/service/task.service.ts | 27 ++-- .../features/task/view/task/task.component.ts | 16 +- src/app/layout/main/main.component.ts | 2 +- src/app/shared/services/snack-bar.service.ts | 38 +++++ src/styles.css | 4 + src/styles/overrides.css | 4 + 17 files changed, 364 insertions(+), 26 deletions(-) create mode 100644 src/app/features/task/components/inclusion-form/include-task-form/include-task-form.component.ts create mode 100644 src/app/features/task/components/inclusion-form/inclusion-form.component.ts create mode 100644 src/app/features/task/constants/create-task-form.ts create mode 100644 src/app/shared/services/snack-bar.service.ts create mode 100644 src/styles/overrides.css diff --git a/angular.json b/angular.json index 83a8c48..3394018 100644 --- a/angular.json +++ b/angular.json @@ -27,7 +27,7 @@ } ], "styles": [ - "@angular/material/prebuilt-themes/azure-blue.css", + "@angular/material/prebuilt-themes/magenta-violet.css", "src/styles.css" ], "scripts": [], diff --git a/db.json b/db.json index b694e99..fc82235 100644 --- a/db.json +++ b/db.json @@ -1,5 +1,30 @@ { - "tasks": [], + "tasks": [ + { + "id": "a2e6", + "title": "Teste", + "categoryId": "1", + "isCompleted": false + }, + { + "id": "9992", + "title": "Testeasdas", + "categoryId": "1", + "isCompleted": false + }, + { + "id": "c544", + "title": "treinar", + "categoryId": "1", + "isCompleted": false + }, + { + "id": "ecc9", + "title": "treinar 2", + "categoryId": "1", + "isCompleted": false + } + ], "categories": [ { "id": "1", @@ -27,4 +52,4 @@ "color": "purple" } ] -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2232b3c..9f64c8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "my-task-board-angular", "version": "0.0.0", "dependencies": { + "@angular/animations": "^19.2.6", "@angular/cdk": "^19.2.9", "@angular/common": "^19.2.0", "@angular/compiler": "^19.2.0", @@ -393,6 +394,22 @@ "tslib": "^2.1.0" } }, + "node_modules/@angular/animations": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.6.tgz", + "integrity": "sha512-0Ei7pKXpq0eoijakRB+TQCh2EB02ReYUzRkhdw5kbQLOlTftBWWnMNn2qRfKU6cra+RyRXU8c34ZkEw6K7hZAw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.6", + "@angular/core": "19.2.6" + } + }, "node_modules/@angular/build": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.7.tgz", diff --git a/package.json b/package.json index b183998..1891e1c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "private": true, "dependencies": { + "@angular/animations": "^19.2.6", "@angular/cdk": "^19.2.9", "@angular/common": "^19.2.0", "@angular/compiler": "^19.2.0", diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 618d9f2..095d26c 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -2,8 +2,9 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; +import { provideAnimations } from '@angular/platform-browser/animations'; import { provideHttpClient, withFetch } from '@angular/common/http' -import { providerThemeInitializer } from './initializers/theme-initializer'; +import { providerThemeInitializer } from './initializers/theme-initializer'; export const appConfig: ApplicationConfig = { providers: [ @@ -11,6 +12,7 @@ export const appConfig: ApplicationConfig = { provideRouter(routes), provideClientHydration(withEventReplay()), provideHttpClient(withFetch()), + provideAnimations(), providerThemeInitializer, ] }; diff --git a/src/app/features/category/constants/category-colors.ts b/src/app/features/category/constants/category-colors.ts index fcdeb34..18d3039 100644 --- a/src/app/features/category/constants/category-colors.ts +++ b/src/app/features/category/constants/category-colors.ts @@ -1,7 +1,15 @@ -export const categoryBackgroundColors: Record = { +export const categoryBackgroundColors: Record = { green: 'bg-green-600', - orange: 'bg-orange-600', + orange: 'bg-yellow-600', red: 'bg-red-600', blue: 'bg-blue-600', purple: 'bg-purple-600', }; + +export const categoryBackgroundIdColors: Record = { + '1': 'bg-green-600', + '2': 'bg-yellow-600', + '3': 'bg-red-600', + '4': 'bg-blue-600', + '5': 'bg-purple-600', +}; diff --git a/src/app/features/category/services/category.service.ts b/src/app/features/category/services/category.service.ts index da5c438..992b412 100644 --- a/src/app/features/category/services/category.service.ts +++ b/src/app/features/category/services/category.service.ts @@ -1,4 +1,4 @@ -import { inject, Injectable } from '@angular/core'; +import { inject, Injectable, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Category } from '../model/category.model'; @@ -17,4 +17,6 @@ export class CategoryService { public categories = toSignal(this.categories$, { initialValue: [] as Category[], }); + + public selectedCategoryId = signal('1') } diff --git a/src/app/features/task/components/inclusion-form/include-task-form/include-task-form.component.ts b/src/app/features/task/components/inclusion-form/include-task-form/include-task-form.component.ts new file mode 100644 index 0000000..a118894 --- /dev/null +++ b/src/app/features/task/components/inclusion-form/include-task-form/include-task-form.component.ts @@ -0,0 +1,150 @@ +import { FormGroup } from '@angular/forms'; +import { CategoryService } from './../../../../category/services/category.service'; +import { + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + inject, + OnInit, +} from '@angular/core'; + +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectChange, MatSelectModule } from '@angular/material/select'; +import { createTaskForm } from '../../../constants/create-task-form'; +import { Task } from '../../../model/task.model'; +import { TaskService } from '../../../service/task.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { delay, finalize } from 'rxjs/operators'; + +import { NgClass } from '@angular/common'; +import { SnackBarService } from '../../../../../shared/services/snack-bar.service'; +import { throws } from 'assert'; + +const MODULES = [ + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatSelectModule, + FormsModule, + ReactiveFormsModule, +]; + +// const COMPONENTS: never[] = []; + +const COMMONS = [NgClass]; + + +@Component({ + selector: 'app-include-task-form', + standalone: true, + imports: [...COMMONS, ...MODULES], + template: ` +
+ + Tarefas + + Aperte enter para adicionar + + + Categorias + + @for(category of categories(); track category.id) { + {{ category.name }} + } + + +
+ `, + styles: ``, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class IncludeTaskFormComponent { + private readonly categoryService = inject(CategoryService); + + private readonly taskService = inject(TaskService); + + private readonly snackBarService = inject(SnackBarService); + + private readonly destroy$ = inject(DestroyRef); + + public readonly categories = this.categoryService.categories; + + public readonly newsTaskForm = createTaskForm(); + + + public selectionChangeHandler(event: MatSelectChange): void { + const categoryId = event.value; + this.categoryService.selectedCategoryId.set(categoryId); + } + + public onEnterToaddATask(): void { + if (this.newsTaskForm.invalid) return; + + this.taskService.isLoadingTask.set(true); + + const { categoryId, title } = this.newsTaskForm.value; + + const newTask: Partial = { + title, + categoryId, + isCompleted: false, + }; + + this.taskService + .createTask(newTask) + .pipe( + delay(4000), + finalize(() => this.taskService.isLoadingTask.set(false)), + takeUntilDestroyed(this.destroy$) + ) + .subscribe({ + next: (task) => this.taskService.insertATaskInTheTasksList(task), + error: (error) => { + this.snackBarConfigHandler(error.message) + }, + complete: () => this.snackBarConfigHandler('Tarefa concluida!') + }); + } + + public isIncludeTaskFormDisabled = computed(() => { + if (this.taskService.isLoadingTask()) { + this.newsTaskForm.disable(); + + return this.taskService.isLoadingTask(); + } + + this.newsTaskForm.enable(); + + return this.taskService.isLoadingTask(); + }); + + public snackBarConfigHandler(message : string): void { + this.snackBarService.showSnackBar( + message + ,4000 + ,'end' + ,'top' + ) + } +} diff --git a/src/app/features/task/components/inclusion-form/inclusion-form.component.ts b/src/app/features/task/components/inclusion-form/inclusion-form.component.ts new file mode 100644 index 0000000..7532993 --- /dev/null +++ b/src/app/features/task/components/inclusion-form/inclusion-form.component.ts @@ -0,0 +1,42 @@ +import { TaskService } from './../../service/task.service'; +import { Component, inject } from '@angular/core'; +import { IncludeTaskFormComponent } from './include-task-form/include-task-form.component'; +import { CategoryService } from '../../../category/services/category.service'; +import { categoryBackgroundIdColors } from '../../../category/constants/category-colors'; +import { NgClass } from '@angular/common'; + +const COMPONENTS = [IncludeTaskFormComponent] +const COMMONS = [NgClass]; + +@Component({ + selector: 'app-inclusion-form', + imports: [...COMPONENTS, ...COMMONS], + template: ` +
+ + +
+ +
+
+ `, + standalone: true, + styles: ``, +}) +export class InclusionFormComponent { + private readonly categoryService = inject(CategoryService); + + public readonly taskService = inject(TaskService); + + public readonly selectedCategoryId = this.categoryService.selectedCategoryId; + + public colorVariants = categoryBackgroundIdColors; +} diff --git a/src/app/features/task/constants/create-task-form.ts b/src/app/features/task/constants/create-task-form.ts new file mode 100644 index 0000000..0ef621b --- /dev/null +++ b/src/app/features/task/constants/create-task-form.ts @@ -0,0 +1,27 @@ +import { Task } from './../model/task.model'; +import { inject } from "@angular/core"; +import { FormControl, FormGroup, NonNullableFormBuilder, Validators } from "@angular/forms"; + +type TaskFormControl = { + title: FormControl; + categoryId: FormControl; +}; + +export function createTaskForm(): FormGroup { + const formBuilder = inject(NonNullableFormBuilder); + + return formBuilder.group({ + title: new FormControl('', { + validators: [Validators.required, Validators.minLength(3)], + nonNullable: true, + }), + categoryId: new FormControl('1', { + validators: [Validators.required], + nonNullable: true, + }), + }); +} + +export type TaskFormGroup = ReturnType; + +export type TaskFormValue = ReturnType; diff --git a/src/app/features/task/service/task.service.spec.ts b/src/app/features/task/service/task.service.spec.ts index cb0fbb3..80c7be4 100644 --- a/src/app/features/task/service/task.service.spec.ts +++ b/src/app/features/task/service/task.service.spec.ts @@ -3,6 +3,7 @@ import { TestBed, waitForAsync } from '@angular/core/testing'; import { HttpErrorResponse, provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { TaskService } from './task.service'; +import { Task } from '../model/task.model'; describe('TaskService', () => { let taskService: TaskService; @@ -75,16 +76,16 @@ describe('TaskService', () => { }); describe('creatTask', () => { - it('should create a new task', waitForAsync(() => { - taskService.createTask(MOCKED_TASK).subscribe(() => { - expect(taskService.tasks()[0]).toEqual(MOCKED_TASK); - expect(taskService.tasks().length).toEqual(1); + it('should create a new task with waitForAsync', waitForAsync(() => { + let task: Task | undefined; + taskService.createTask(MOCKED_TASK).subscribe((response) => { + task = response }); const req = httpTestingController.expectOne(`${baseURL}/tasks`); req.flush(MOCKED_TASK); - + expect(task).toEqual(MOCKED_TASK); expect(req.request.method).toEqual('POST'); })); diff --git a/src/app/features/task/service/task.service.ts b/src/app/features/task/service/task.service.ts index 3341ce7..8e07a31 100644 --- a/src/app/features/task/service/task.service.ts +++ b/src/app/features/task/service/task.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable, computed, inject, signal } from '@angular/core'; import { environment } from '../../../../env/environment.prod'; import { Observable, tap } from 'rxjs'; +import { setThrowInvalidWriteToSignalError } from '@angular/core/primitives/signals'; @Injectable({ providedIn: 'root', @@ -10,11 +11,13 @@ import { Observable, tap } from 'rxjs'; export class TaskService { private readonly httpClient = inject(HttpClient); - public tasks = signal([]); + private readonly _apiUrl = environment.apiUrl; - public numberOfTasks = computed(() => this.tasks().length); + public readonly tasks = signal([]); - public readonly _apiUrl = environment.apiUrl; + public readonly numberOfTasks = computed(() => this.tasks().length); + + public isLoadingTask = signal(false); public getTasks(): Observable { return this.httpClient.get(`${this._apiUrl}/tasks`).pipe( @@ -32,14 +35,13 @@ export class TaskService { public createTask(task: Partial): Observable { return this.httpClient .post(`${this._apiUrl}/tasks`, task) - .pipe(tap((tasks) => this.insertATaskInTheTasksList(tasks))); } public insertATaskInTheTasksList(newTask: Task): void { - const updatedTasks = [...this.tasks(), newTask]; - const sortedTasks = this.getSortedTasks(updatedTasks); - - this.tasks.set(sortedTasks); + this.tasks.update(tasks => { + const newTasksList = [...tasks, newTask]; + return this.getSortedTasks(newTasksList); + }); } public insertATasksList(newTask: Task): void { @@ -54,7 +56,10 @@ export class TaskService { .pipe(tap((tasks) => this.updateTaskInTheTasksList(tasks))); } - public updateIsCompletedStatus(taskId: string, isCompleted: boolean ): Observable { + public updateIsCompletedStatus( + taskId: string, + isCompleted: boolean + ): Observable { return this.httpClient .patch(`${this._apiUrl}/tasks/${taskId}`, { isCompleted }) .pipe(tap((tasks) => this.updateTaskInTheTasksList(tasks))); @@ -73,8 +78,8 @@ export class TaskService { public deleteTask(taskId: string): Observable { return this.httpClient - .delete(`${this._apiUrl}/tasks/${taskId}`) - .pipe(tap(() => this.deleteATaksInTheTasksList(taskId))); + .delete(`${this._apiUrl}/tasks/${taskId}`) + .pipe(tap(() => this.deleteATaksInTheTasksList(taskId))); } public deleteATaksInTheTasksList(taskId: string): void { diff --git a/src/app/features/task/view/task/task.component.ts b/src/app/features/task/view/task/task.component.ts index 3c50c38..055279b 100644 --- a/src/app/features/task/view/task/task.component.ts +++ b/src/app/features/task/view/task/task.component.ts @@ -1,10 +1,22 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { InclusionFormComponent } from '../../components/inclusion-form/inclusion-form.component'; + +const COMPONENTS = [InclusionFormComponent]; + @Component({ selector: 'app-task', standalone: true, - imports: [], - template: `

task works!

`, + imports: [...COMPONENTS], + template: `
+ + Meu quadro de tarefas + + + + + +
`, styles: ``, changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/src/app/layout/main/main.component.ts b/src/app/layout/main/main.component.ts index 6ae385f..72515d2 100644 --- a/src/app/layout/main/main.component.ts +++ b/src/app/layout/main/main.component.ts @@ -19,7 +19,7 @@ const MODULES = [MatDivider]; - + `, styles: ``, diff --git a/src/app/shared/services/snack-bar.service.ts b/src/app/shared/services/snack-bar.service.ts new file mode 100644 index 0000000..f45f132 --- /dev/null +++ b/src/app/shared/services/snack-bar.service.ts @@ -0,0 +1,38 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { MatSnackBar, MatSnackBarHorizontalPosition, MatSnackBarVerticalPosition } from '@angular/material/snack-bar' + +@Injectable({ + providedIn: 'root', +}) +export class SnackBarService { + public message = signal(''); + + private _snackBar = inject(MatSnackBar); + + public durationInMiliSeconds = 3000; + + public horizontalPosition: MatSnackBarHorizontalPosition = 'end'; + public verticalPosition: MatSnackBarVerticalPosition = 'top'; + + public showSnackBar( + message: string, + duration: number, + horizontalPosition: MatSnackBarHorizontalPosition, + verticalPosition: MatSnackBarVerticalPosition + ): void { + this.message.set(message); + this.durationInMiliSeconds = duration; + this.horizontalPosition = horizontalPosition; + this.verticalPosition = verticalPosition; + + this.openSnackBar(); + }; + + private openSnackBar(): void { + this._snackBar.open(this.message(), '❌', { + duration: this.durationInMiliSeconds, + horizontalPosition: this.horizontalPosition, + verticalPosition: this.verticalPosition, + }); + } +} diff --git a/src/styles.css b/src/styles.css index c86802d..067d7c3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,5 +1,6 @@ /* You can add global styles to this file, and also import other style files */ @import "tailwindcss"; +@import "./styles/overrides.css"; /* Tema claro */ .light-theme { @@ -18,3 +19,6 @@ body { color: var(--text-color); transition: background-color 0.3s, color 0.3s; } + +html, body { height: 100%; } +body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } diff --git a/src/styles/overrides.css b/src/styles/overrides.css new file mode 100644 index 0000000..f86c816 --- /dev/null +++ b/src/styles/overrides.css @@ -0,0 +1,4 @@ +.mat-mdc-form-field-hint-wrapper { + padding: 0px !important; + margin: 0px !important; +}