diff --git a/src/app/components/application-list/application-list.component.scss b/src/app/components/application-list/application-list.component.scss index 18a305a..a0abbed 100644 --- a/src/app/components/application-list/application-list.component.scss +++ b/src/app/components/application-list/application-list.component.scss @@ -110,6 +110,7 @@ button.secondary-btn:hover { /* Applicant card layout */ .listItem { display: flex; + justify-content: space-between; align-items: center; gap: 1.2rem; padding: 1rem 1.4rem; @@ -149,7 +150,8 @@ button.secondary-btn:hover { } .delete-button { - margin-left: 12px; + margin-left: auto; + margin-right: 1rem; width: 40px; height: 40px; min-width: 40px; diff --git a/src/app/components/application/application.component.ts b/src/app/components/application/application.component.ts index 087bb84..f4d72f2 100644 --- a/src/app/components/application/application.component.ts +++ b/src/app/components/application/application.component.ts @@ -12,6 +12,7 @@ import { SocketIOService } from '../../services/socket-io.service'; import { MatSnackBar } from '@angular/material/snack-bar'; import { trigger, transition } from '@angular/animations'; import { slideLeft, slideRight } from './slide.animation'; +import { tap } from 'rxjs'; @Component({ selector: 'app-application', @@ -68,21 +69,22 @@ export class ApplicationComponent implements OnInit { ngOnInit(): void { - if (this.dataService.cachedApplicationList.length > 0) { - this.applicationList.set(this.dataService.cachedApplicationList); + if (this.dataService.cachedApplicationList().length > 0) { + this.applicationList.set(this.dataService.cachedApplicationList()); this.initializeApplication(); - } else { - this.dataService.loadCandidateList().subscribe({ - next: (data) => { - this.applicationList.set(data); - this.initializeApplication(); - }, - error: (error) => { - console.error('Error loading candidate list:', error); - alert('Error loading candidate list'); - } - }); + return; } + this.dataService.loadCandidateList().subscribe({ + next: (data) => { + this.applicationList.set(data); + this.initializeApplication(); + }, + error: (error) => { + console.error('Error loading candidate list:', error); + alert('Error loading candidate list'); + } + }); + } editApplication() { @@ -108,7 +110,7 @@ export class ApplicationComponent implements OnInit { if (this.currentIndex() !== foundIndex) { this.currentIndex.set(foundIndex); - this.loadApplication(applicationId); + this.loadApplication(applicationId)?.subscribe(); } } @@ -118,7 +120,7 @@ export class ApplicationComponent implements OnInit { } this.currentApplication.set(null); - this.dataService.getApplicationDetails(id).subscribe({ + return this.dataService.getApplicationDetails(id).pipe(tap({ next: (data) => { this.currentApplication.set(data); const currentRoute = this.router.url; @@ -131,7 +133,7 @@ export class ApplicationComponent implements OnInit { console.error('Error loading application details:', error); alert('Error loading application details'); } - }); + })); } goToPrevious() { @@ -139,7 +141,7 @@ export class ApplicationComponent implements OnInit { const newIndex = this.currentIndex() - 1; this.currentIndex.set(newIndex); const prevId = this.applicationList()[newIndex].id; - this.loadApplication(prevId); + this.loadApplication(prevId)?.subscribe(); } } @@ -148,7 +150,7 @@ export class ApplicationComponent implements OnInit { const newIndex = this.currentIndex() + 1; this.currentIndex.set(newIndex); const nextId = this.applicationList()[newIndex].id; - this.loadApplication(nextId); + this.loadApplication(nextId)?.subscribe(); } } diff --git a/src/app/components/image-input/image-input.component.html b/src/app/components/image-input/image-input.component.html index eac9d09..f087b6d 100644 --- a/src/app/components/image-input/image-input.component.html +++ b/src/app/components/image-input/image-input.component.html @@ -1,5 +1,5 @@
- Profile Photo + Profile Photo*
@@ -24,8 +24,7 @@ Drag and drop here diff --git a/src/app/components/image-input/image-input.component.ts b/src/app/components/image-input/image-input.component.ts index 6db9bb2..69ce39d 100644 --- a/src/app/components/image-input/image-input.component.ts +++ b/src/app/components/image-input/image-input.component.ts @@ -1,5 +1,5 @@ -import { Component, effect, forwardRef, inject, input } from '@angular/core'; -import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; +import { Component, effect, forwardRef, inject, input, viewChild } from '@angular/core'; +import { ControlValueAccessor, FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -25,13 +25,15 @@ import { CommonModule } from '@angular/common'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ImageInputComponent), multi: true - } + }, ], templateUrl: './image-input.component.html', styleUrl: './image-input.component.scss' }) export class ImageInputComponent implements ControlValueAccessor { + fileInput = viewChild('fileInput'); value: File | null = null; + touched = false; disabled = false; previewUrl: string | null = null; src = input(null); @@ -54,9 +56,10 @@ export class ImageInputComponent implements ControlValueAccessor { if (value) { this.previewUrl = URL.createObjectURL(value); } else { - this.previewUrl = null; + this.previewUrl = null; } } + registerOnChange(fn: any): void { this.onChange = fn; } @@ -71,16 +74,21 @@ export class ImageInputComponent implements ControlValueAccessor { setFileData(event: Event): void { + this.markAsTouched(); const input = event.target as HTMLInputElement; if (input?.files?.[0]) { this.value = input.files[0]; - this.previewUrl = URL.createObjectURL(this.value); - console.log(this.previewUrl) this.onChange(this.value); } } + markAsTouched() { + if (this.touched) return; + this.onTouched(); + this.touched = true; + } + clear() { this.value = null; this.isDisplayOnly = false; diff --git a/src/app/components/leaflet-map/leaflet-map.component.ts b/src/app/components/leaflet-map/leaflet-map.component.ts index 11acda6..4de9567 100644 --- a/src/app/components/leaflet-map/leaflet-map.component.ts +++ b/src/app/components/leaflet-map/leaflet-map.component.ts @@ -86,7 +86,6 @@ export class LeafletMapComponent implements AfterViewInit, ControlValueAccessor if (exact) this.onSelectCity(exact.name); else this.onTouched(); - console.log(this.marker); } diff --git a/src/app/components/registration/registration.component.html b/src/app/components/registration/registration.component.html index 84ef5b5..f2ee90f 100644 --- a/src/app/components/registration/registration.component.html +++ b/src/app/components/registration/registration.component.html @@ -26,6 +26,9 @@
+ @if (form.get('profileImage')?.touched && form.get('profileImage')?.hasError('required')) { + Image is required + }
@@ -43,6 +46,9 @@ @if (form.get('email')?.hasError('email')) { Invalid email + } + @if (form.get('email')?.hasError('required')) { + Email is required } @@ -105,7 +111,7 @@ - diff --git a/src/app/components/registration/registration.component.scss b/src/app/components/registration/registration.component.scss index 8984bc3..5aa0ebb 100644 --- a/src/app/components/registration/registration.component.scss +++ b/src/app/components/registration/registration.component.scss @@ -51,6 +51,8 @@ display: flex; justify-content: center; margin-bottom: 1rem; + align-items: center; + flex-direction: column; } .image-upload img { @@ -101,7 +103,6 @@ mat-error { } - /* Buttons */ button { cursor: pointer; diff --git a/src/app/components/registration/registration.component.ts b/src/app/components/registration/registration.component.ts index e1b05b4..b69cb7f 100644 --- a/src/app/components/registration/registration.component.ts +++ b/src/app/components/registration/registration.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, ElementRef, inject, input, OnInit, QueryList, signal, ViewChildren } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, inject, input, OnInit, QueryList, Renderer2, signal, ViewChildren } from '@angular/core'; import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; @@ -44,6 +44,7 @@ export class RegistrationComponent implements OnInit { activatedRoute = inject(ActivatedRoute); snackBar = inject(MatSnackBar) socketService = inject(SocketIOService); + renderer = inject(Renderer2); originalImageUrl: string | null = null; previewUrl: string | null = null; @@ -118,6 +119,7 @@ export class RegistrationComponent implements OnInit { } onSubmit() { + this.form.markAllAsTouched(); if (!this.form.valid) { this.scrollToFirstInvalidField(); return; @@ -156,14 +158,18 @@ export class RegistrationComponent implements OnInit { }); } else { this.dataService.submitCandidateForm(formData).subscribe({ - next: (newCandidate) => { + next: (newCandidate: any) => { this.snackBar.open('✅ Application saved!', 'Close', { duration: 5000, horizontalPosition: 'center', verticalPosition: 'top', }); + this.dataService.cachedApplicationList.update((data) => { + return { ...data, newCandidate }; + }) this.socketService.socket.emit('candidateRegistered', newCandidate); - this.form.reset(); + this.applicationId.set(newCandidate.id); + this.router.navigate([`/application/${this.applicationId()}`]) }, error: err => alert('Error submitting form'), }); diff --git a/src/app/services/candidate-data.service.ts b/src/app/services/candidate-data.service.ts index 01d0132..101e1ac 100644 --- a/src/app/services/candidate-data.service.ts +++ b/src/app/services/candidate-data.service.ts @@ -1,16 +1,16 @@ import { HttpClient } from "@angular/common/http"; import { inject, Injectable, signal } from "@angular/core"; -import { environment } from "../../environments/environment.development"; +import { environment } from "../../environments/environment"; import { delay, tap } from "rxjs"; @Injectable({ providedIn: 'root' }) export class CandidateDataService { - httpClient = inject(HttpClient) - isCandidatesListLoading = signal(false) - isApplicationDetailsLoading = signal(false) - cachedApplicationList: any[] = [] + httpClient = inject(HttpClient); + isCandidatesListLoading = signal(false); + isApplicationDetailsLoading = signal(false); + cachedApplicationList = signal([]); loadCandidateList() { this.isCandidatesListLoading.set(true) @@ -18,7 +18,7 @@ export class CandidateDataService { delay(500), tap((data) => { this.isCandidatesListLoading.set(false); - this.cachedApplicationList = data; + this.cachedApplicationList.set(data); }) ); } @@ -45,7 +45,7 @@ export class CandidateDataService { deleteCandidate(id: number) { return this.httpClient.delete(`${environment.hostUrl}/app/candidate/${id}`).pipe( tap(() => { - this.cachedApplicationList = this.cachedApplicationList.filter(c => c.id !== id); + this.cachedApplicationList.set(this.cachedApplicationList().filter(c => c.id !== id)); }) ); } diff --git a/src/app/services/socket-io.service.ts b/src/app/services/socket-io.service.ts index 915781f..d65156e 100644 --- a/src/app/services/socket-io.service.ts +++ b/src/app/services/socket-io.service.ts @@ -1,17 +1,19 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { io } from "socket.io-client"; +import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root' }) export class SocketIOService { - socket = io('ws://localhost:3000'); + socket = io(`${environment.socketUrl}`); onCandidateRegistered(): Observable { return new Observable(observer => { this.socket.on('candidateRegistered', (data) => { + console.log(data); observer.next(data); }); }); diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index 9b87101..75f81f6 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,5 +1,6 @@ export const environment = { hostUrl: 'http://localhost:3000', - mapTilerApiKey: '9LJZ0OppHyT3LzvQW3ce' + mapTilerApiKey: '9LJZ0OppHyT3LzvQW3ce', + socketUrl: 'ws://localhost:3000' }; diff --git a/src/environments/environment.remote-api.ts b/src/environments/environment.remote-api.ts index ddbb408..63d6485 100644 --- a/src/environments/environment.remote-api.ts +++ b/src/environments/environment.remote-api.ts @@ -1,5 +1,5 @@ export const environment = { hostUrl: 'https://iisa.novikov.click', - mapTilerApiKey: '9LJZ0OppHyT3LzvQW3ce' - + mapTilerApiKey: '9LJZ0OppHyT3LzvQW3ce', + socketUrl: 'https://iisa.novikov.click' }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 86b78f9..7da6cea 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,4 +1,5 @@ export const environment = { hostUrl: '', - mapTilerApiKey:'' + mapTilerApiKey: '', + socketUrl: '' };