diff --git a/src/app/pages/task-board/task-board.component.html b/src/app/pages/task-board/task-board.component.html index daaf846..79e7235 100644 --- a/src/app/pages/task-board/task-board.component.html +++ b/src/app/pages/task-board/task-board.component.html @@ -1,22 +1,33 @@

Task Tracker Pro

-
+
+ formControlName="title" + placeholder="Task title" + class="form-control" /> +
+ Title is required + Title must be at least 3 characters +
+ formControlName="description" + placeholder="Task description" + class="form-control">
- +
@if (isLoading(); as loading) { @@ -36,10 +47,10 @@

To Do

-
@@ -58,10 +69,12 @@

In Progress

}
- -
@@ -80,7 +93,7 @@

Done

}
-
diff --git a/src/app/pages/task-board/task-board.component.scss b/src/app/pages/task-board/task-board.component.scss index 841bb90..91a2daa 100644 --- a/src/app/pages/task-board/task-board.component.scss +++ b/src/app/pages/task-board/task-board.component.scss @@ -1,6 +1,6 @@ .task-board { padding: 1rem; - max-width: 1200px; + max-width: 1400px; margin: 0 auto; h2 { @@ -9,52 +9,168 @@ font-size: 2rem; } - .create-task { + // Button Base Styles + %button-base { + border: none; + border-radius: 4px; + padding: 0.65rem 1rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + position: relative; + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + background-color: #6c757d !important; + color: #fff !important; + transform: none !important; + box-shadow: none !important; + pointer-events: none; + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + } + } + + &:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + &:active:not(:disabled) { + transform: translateY(0); + box-shadow: none; + } + + &:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + } + + &:focus:not(:focus-visible) { + box-shadow: none; + } + } + + // Button Variants + .button-primary { + @extend %button-base; + background: #28a745; + color: white; + + &:hover:not(:disabled) { + background: #218838; + } + + &:focus { + box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25); + } + } + + .button-secondary { + @extend %button-base; + background: #007bff; + color: white; + + &:hover:not(:disabled) { + background: #0056b3; + } + + &:focus { + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + } + } + + .button-danger { + @extend %button-base; + background: #dc3545; + color: white; + + &:hover:not(:disabled) { + background: #c82333; + } + + &:focus { + box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25); + } + } + + form { background-color: #f8f9fa; - padding: 1.5rem; + padding: 1rem; border-radius: 8px; margin-bottom: 2rem; box-shadow: 0 0 4px rgba(0, 0, 0, 0.05); .form-group { + display: flex; + flex-direction: column; margin-bottom: 1rem; + position: relative; - input, - textarea { - width: 100%; + .form-control { padding: 0.75rem; border: 1px solid #dee2e6; border-radius: 4px; font-family: inherit; font-size: 1rem; + transition: all 0.2s ease; &:focus { outline: none; border-color: #007bff; box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); } + + &.ng-invalid.ng-touched { + border-color: #dc3545; + box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25); + } } - textarea { - min-height: 80px; + textarea.form-control { resize: vertical; } - } - .create-button { - background: #28a745; - color: white; - border: none; - border-radius: 4px; - padding: 0.75rem 1.5rem; - font-size: 1rem; - cursor: pointer; - transition: background-color 0.2s; + .error-message { + color: #dc3545; + font-size: 0.875rem; + margin-top: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + + span { + display: block; + padding-left: 0.5rem; + position: relative; - &:hover { - background: #218838; + &::before { + content: '•'; + position: absolute; + left: 0; + } + } } } + + button[type='submit'] { + @extend .button-primary; + width: 100%; + } } .loading { @@ -64,14 +180,14 @@ } .columns { - display: flex; - gap: 1rem; - justify-content: space-between; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + width: 100%; } .column { - flex: 1 1 30%; + min-width: 0; background-color: #f8f9fa; padding: 1rem; border-radius: 8px; @@ -80,6 +196,8 @@ h3 { margin-bottom: 1rem; font-size: 1.2rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid #e9ecef; } .task { @@ -107,35 +225,17 @@ display: flex; gap: 0.5rem; justify-content: flex-end; - } - .action-button { - background: #007bff; - color: white; - border: none; - border-radius: 4px; - padding: 0.4rem 0.7rem; - font-size: 0.8rem; - cursor: pointer; - transition: background-color 0.2s; - - &:hover { - background: #0056b3; + .action-button { + @extend .button-secondary; + padding: 0.4rem 0.7rem; + font-size: 0.8rem; } - } - .delete-button { - background: #dc3545; - color: white; - border: none; - border-radius: 4px; - padding: 0.4rem 0.7rem; - font-size: 0.8rem; - cursor: pointer; - transition: background-color 0.2s; - - &:hover { - background: #c82333; + .delete-button { + @extend .button-danger; + padding: 0.4rem 0.7rem; + font-size: 0.8rem; } } } @@ -143,21 +243,16 @@ } // Mobile styles -@media screen and (max-width: 768px) { +@media screen and (max-width: 1024px) { .task-board { padding: 0.5rem; .columns { - flex-direction: column; - gap: 1.5rem; - } - - .column { - flex: 1 1 100%; - width: 100%; + grid-template-columns: 1fr; + gap: 1rem; } - .create-task { + form { padding: 1rem; } } diff --git a/src/app/pages/task-board/task-board.component.ts b/src/app/pages/task-board/task-board.component.ts index 3eeef5c..7549b54 100644 --- a/src/app/pages/task-board/task-board.component.ts +++ b/src/app/pages/task-board/task-board.component.ts @@ -1,43 +1,50 @@ import { Component, OnInit, inject, Signal } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; +import { + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; import { TaskStore } from '../../stores/task.store'; import { Task } from '../../interfaces/task'; @Component({ selector: 'app-task-board', standalone: true, - imports: [CommonModule, FormsModule], + imports: [CommonModule, ReactiveFormsModule], templateUrl: './task-board.component.html', styleUrls: ['./task-board.component.scss'], providers: [TaskStore], }) export class TaskBoardComponent implements OnInit { readonly store = inject(TaskStore); + private readonly fb = inject(FormBuilder); readonly todo: Signal = this.store.tasksTodo; readonly inProgress: Signal = this.store.tasksInProgress; readonly done: Signal = this.store.tasksDone; readonly isLoading = this.store.isLoading; - newTaskTitle = ''; - newTaskDescription = ''; + taskForm: FormGroup = this.fb.group({ + title: ['', [Validators.required, Validators.minLength(3)]], + description: [''], + }); ngOnInit(): void { this.store.fetchTasks(); } async createTask() { - if (!this.newTaskTitle.trim()) return; + if (this.taskForm.invalid) return; try { await this.store.createTask({ - title: this.newTaskTitle, - description: this.newTaskDescription, + title: this.taskForm.get('title')?.value, + description: this.taskForm.get('description')?.value, status: 'todo', }); - this.newTaskTitle = ''; - this.newTaskDescription = ''; + this.taskForm.reset(); } catch (error) { console.error('Error creating task:', error); } diff --git a/src/app/services/task.service.ts b/src/app/services/task.service.ts index f7a0a1b..c1e3ddf 100644 --- a/src/app/services/task.service.ts +++ b/src/app/services/task.service.ts @@ -3,15 +3,35 @@ import { Observable, of } from 'rxjs'; import { delay, tap } from 'rxjs/operators'; import { Task } from '../interfaces/task'; import { HOUSEHOLD_TASKS } from '../mocks/household-tasks'; +// import { HttpClient, HttpParams } from '@angular/common/http'; @Injectable({ providedIn: 'root' }) export class TaskService { private readonly MOCK_DATA: Task[] = [...HOUSEHOLD_TASKS]; + // private readonly API_URL = 'https://api.example.com/tasks'; + + // Expected CRUD API Endpoints: + // GET /tasks?page=1&pageSize=10 - Get paginated tasks + // POST /tasks - Create new task + // DELETE /tasks/:id - Delete task by ID + // PATCH /tasks/:id/status - Update task status + // GET /tasks/:id - Get single task (if needed) + // PUT /tasks/:id - Update entire task (if needed) + + // constructor(private http: HttpClient) {} getTasks( page: number, pageSize: number ): Observable<{ tasks: Task[]; totalPages: number }> { + // Real API implementation: + // GET /tasks?page=1&pageSize=10 + // Returns: { tasks: Task[], totalPages: number } + // const params = new HttpParams() + // .set('page', page.toString()) + // .set('pageSize', pageSize.toString()); + // return this.http.get<{ tasks: Task[]; totalPages: number }>(this.API_URL, { params }); + console.log('[Service - Request] Fetching tasks', { page, pageSize }); const start = (page - 1) * pageSize; const end = start + pageSize; @@ -30,6 +50,12 @@ export class TaskService { } createTask(task: Omit): Observable { + // Real API implementation: + // POST /tasks + // Body: { title: string, description: string, status: string, ... } + // Returns: Task (with id and createdAt) + // return this.http.post(this.API_URL, task); + console.log('[Service - Request] Creating task', task); const newTask: Task = { ...task, @@ -46,6 +72,11 @@ export class TaskService { } deleteTask(taskId: string): Observable { + // Real API implementation: + // DELETE /tasks/:id + // Returns: boolean (success/failure) + // return this.http.delete(`${this.API_URL}/${taskId}`); + console.log('[Service - Request] Deleting task', taskId); const index = this.MOCK_DATA.findIndex(task => task.id === taskId); if (index > -1) { @@ -69,6 +100,12 @@ export class TaskService { taskId: string, newStatus: Task['status'] ): Observable { + // Real API implementation: + // PATCH /tasks/:id/status + // Body: { status: string } + // Returns: boolean (success/failure) + // return this.http.patch(`${this.API_URL}/${taskId}/status`, { status: newStatus }); + console.log('[Service - Request] Updating task status', { taskId, newStatus,