Add toggle messages
This commit is contained in:
parent
d4332dce35
commit
ad272300eb
17 changed files with 364 additions and 26 deletions
|
|
@ -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
29
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
27
src/app/features/task/constants/create-task-form.ts
Normal file
27
src/app/features/task/constants/create-task-form.ts
Normal 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']>;
|
||||
|
|
@ -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');
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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: ``,
|
||||
|
|
|
|||
38
src/app/shared/services/snack-bar.service.ts
Normal file
38
src/app/shared/services/snack-bar.service.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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
4
src/styles/overrides.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.mat-mdc-form-field-hint-wrapper {
|
||||
padding: 0px !important;
|
||||
margin: 0px !important;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue