Add toggle messages

This commit is contained in:
Rayan Konecny do Nascimento 2025-04-17 01:55:58 +00:00
parent d4332dce35
commit ad272300eb
17 changed files with 364 additions and 26 deletions

View file

@ -27,7 +27,7 @@
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"@angular/material/prebuilt-themes/magenta-violet.css",
"src/styles.css"
],
"scripts": [],

29
db.json
View file

@ -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"
}
]
}
}

17
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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,
]
};

View file

@ -1,7 +1,15 @@
export const categoryBackgroundColors: Record<string,string> = {
export const categoryBackgroundColors: Record<string, string> = {
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<string, string> = {
'1': 'bg-green-600',
'2': 'bg-yellow-600',
'3': 'bg-red-600',
'4': 'bg-blue-600',
'5': 'bg-purple-600',
};

View file

@ -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')
}

View file

@ -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: `
<form
[ngClass]="{
'cursor-not-allowed animate-pulse': isIncludeTaskFormDisabled(),
'cursor-pointer': !isIncludeTaskFormDisabled()
}"
autocomplete="off"
class="flex flex-row gap-2 select-none"
[formGroup]="newsTaskForm"
>
<mat-form-field class="w-full">
<mat-label>Tarefas</mat-label>
<input
formControlName="title"
matInput
placeholder="Adicionar tarefa"
(keyup.enter)="onEnterToaddATask()"
/>
<mat-hint class="text-tertiary">Aperte enter para adicionar</mat-hint>
</mat-form-field>
<mat-form-field>
<mat-label>Categorias</mat-label>
<mat-select
formControlName="categoryId"
(selectionChange)="selectionChangeHandler($event)"
(keyup.enter)="onEnterToaddATask()"
>
@for(category of categories(); track category.id) {
<mat-option value="{{ category.id }}">{{ category.name }}</mat-option>
}
</mat-select>
</mat-form-field>
</form>
`,
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<Task> = {
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'
)
}
}

View file

@ -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: `
<div class="grid grid-cols-12 gap-2 mt-8">
<app-include-task-form class="col-span-11" />
<div class="colspan-1 flex items-start mt-2">
<span
[ngClass]="{
'opacity-30': taskService.isLoadingTask(),
'opacity-100': !taskService.isLoadingTask()
}"
class=" {{
colorVariants[selectedCategoryId()]
}} rounded-full w-10 h-10 bg-blue-700"
></span>
</div>
</div>
`,
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;
}

View file

@ -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<string>;
categoryId: FormControl<string>;
};
export function createTaskForm(): FormGroup<TaskFormControl> {
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<typeof createTaskForm>;
export type TaskFormValue = ReturnType<TaskFormGroup['getRawValue']>;

View file

@ -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');
}));

View file

@ -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<Task[]>([]);
private readonly _apiUrl = environment.apiUrl;
public numberOfTasks = computed(() => this.tasks().length);
public readonly tasks = signal<Task[]>([]);
public readonly _apiUrl = environment.apiUrl;
public readonly numberOfTasks = computed(() => this.tasks().length);
public isLoadingTask = signal(false);
public getTasks(): Observable<Task[]> {
return this.httpClient.get<Task[]>(`${this._apiUrl}/tasks`).pipe(
@ -32,14 +35,13 @@ export class TaskService {
public createTask(task: Partial<Task>): Observable<Task> {
return this.httpClient
.post<Task>(`${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<Task> {
public updateIsCompletedStatus(
taskId: string,
isCompleted: boolean
): Observable<Task> {
return this.httpClient
.patch<Task>(`${this._apiUrl}/tasks/${taskId}`, { isCompleted })
.pipe(tap((tasks) => this.updateTaskInTheTasksList(tasks)));
@ -73,8 +78,8 @@ export class TaskService {
public deleteTask(taskId: string): Observable<Task> {
return this.httpClient
.delete<Task>(`${this._apiUrl}/tasks/${taskId}`)
.pipe(tap(() => this.deleteATaksInTheTasksList(taskId)));
.delete<Task>(`${this._apiUrl}/tasks/${taskId}`)
.pipe(tap(() => this.deleteATaksInTheTasksList(taskId)));
}
public deleteATaksInTheTasksList(taskId: string): void {

View file

@ -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: `<p>task works!</p>`,
imports: [...COMPONENTS],
template: ` <div class="flex flex-col mx-10">
<!-- Titulo -->
<span class="font-bold text-4xl">Meu quadro de tarefas</span>
<!-- Formulario -->
<app-inclusion-form />
<!-- Lista de tarefas -->
</div>`,
styles: ``,
changeDetection: ChangeDetectionStrategy.OnPush,
})

View file

@ -19,7 +19,7 @@ const MODULES = [MatDivider];
<mat-divider class="h-full border-2 border-orange-700" />
<!-- Tarefas -->
<app-task class="w-3/4 border-2 border-green-700" />
<app-task class="w-3/4 border-2 border-green-700 pt-10" />
</div>
`,
styles: ``,

View file

@ -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,
});
}
}

View file

@ -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; }

4
src/styles/overrides.css Normal file
View file

@ -0,0 +1,4 @@
.mat-mdc-form-field-hint-wrapper {
padding: 0px !important;
margin: 0px !important;
}