Compare commits

..

4 Commits

Author SHA1 Message Date
e84e1975c6 i hope last fix 2025-08-29 14:26:40 +03:00
12672fa0bf last fix i hope 2025-08-29 14:13:54 +03:00
04f8e56d76 fix 2025-08-29 12:51:43 +03:00
64f51967e3 readme fix 2025-08-28 17:41:39 +03:00
18 changed files with 156 additions and 130 deletions

View File

@@ -1,32 +1,38 @@
# IISAWeb # IISAWeb
IISA frontend application
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.1.6. This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.1.6.
## Development server ## Development server
To start a local development server, run:
To install npm packages run:
```bash ```bash
ng serve npm install
``` ```
> ### With locally hosted backend
>
> Following configuration uses locally hosted backend service https://gitea.novikov.click/EndlessHallucination/IISA
> To start a local development server, run:
>
> ```bash
> ng serve
> ```
> ### With remote backend
>
> Following configuration uses https://iisa.novikov.click server as backend.
> To start a local development server with remote api server, run:
>
> ```bash
> npm run start:remote-api
> ```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building ## Building
To build the project run: To build the project run:
@@ -35,32 +41,4 @@ To build the project run:
ng build ng build
``` ```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

View File

@@ -32,7 +32,7 @@
<!-- Map View --> <!-- Map View -->
@if (viewMode() === 'map') { @if (viewMode() === 'map') {
<app-candidates-map <app-candidates-map
[candidates]="applicationList()" [candidates]="this.dataService.cachedApplicationList()"
[cities]="availableCities()"> [cities]="availableCities()">
</app-candidates-map> </app-candidates-map>
} }

View File

@@ -110,6 +110,7 @@ button.secondary-btn:hover {
/* Applicant card layout */ /* Applicant card layout */
.listItem { .listItem {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
gap: 1.2rem; gap: 1.2rem;
padding: 1rem 1.4rem; padding: 1rem 1.4rem;
@@ -149,7 +150,8 @@ button.secondary-btn:hover {
} }
.delete-button { .delete-button {
margin-left: 12px; margin-left: auto;
margin-right: 1rem;
width: 40px; width: 40px;
height: 40px; height: 40px;
min-width: 40px; min-width: 40px;

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, inject, signal, computed, effect } from '@angular/core'; import { Component, OnInit, inject, signal, computed, effect, OnDestroy } from '@angular/core';
import { CandidateDataService } from '../../services/candidate-data.service'; import { CandidateDataService } from '../../services/candidate-data.service';
import { Router, RouterLink } from '@angular/router'; import { Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@@ -36,23 +36,22 @@ Chart.register(...registerables);
templateUrl: './application-list.component.html', templateUrl: './application-list.component.html',
styleUrls: ['./application-list.component.scss'] styleUrls: ['./application-list.component.scss']
}) })
export class ApplicationListComponent implements OnInit { export class ApplicationListComponent implements OnInit, OnDestroy {
dataService = inject(CandidateDataService); dataService = inject(CandidateDataService);
socketService = inject(SocketIOService); socketService = inject(SocketIOService);
router = inject(Router); router = inject(Router);
snackBar = inject(MatSnackBar) snackBar = inject(MatSnackBar)
environment = environment; environment = environment;
applicationList = signal<any[]>([]);
searchTerm = signal(''); searchTerm = signal('');
filterCity = signal(''); filterCity = signal('');
sortField = signal<string>('fullName'); sortField = signal<string>('fullName');
viewMode = signal<'map' | 'charts'>('map'); viewMode = signal<'map' | 'charts'>('map');
availableCities = signal<City[]>(CITY_LIST); availableCities = signal<City[]>(CITY_LIST);
filteredList = computed(() => { filteredList = computed(() => {
const apps = this.applicationList(); const apps = this.dataService.cachedApplicationList();
const term = this.searchTerm().toLowerCase(); const term = this.searchTerm().toLowerCase();
const city = this.filterCity(); const city = this.filterCity();
@@ -99,7 +98,7 @@ export class ApplicationListComponent implements OnInit {
constructor() { constructor() {
effect(() => { effect(() => {
const data = this.applicationList(); const data = this.dataService.cachedApplicationList();
if (data.length === 0) { if (data.length === 0) {
this.ageChartData.set({ labels: [], datasets: [] }); this.ageChartData.set({ labels: [], datasets: [] });
@@ -177,37 +176,42 @@ export class ApplicationListComponent implements OnInit {
}); });
} }
get isLoading() { get isLoading() {
return this.dataService.isCandidatesListLoading(); return this.dataService.isCandidatesListLoading();
} }
ngOnInit(): void { ngOnInit(): void {
this.socketService.connect();
this.dataService.loadCandidateList().subscribe(data => { this.dataService.loadCandidateList().subscribe(data => {
this.applicationList.set(data); this.dataService.cachedApplicationList.set(data);
this.availableCities.set(this.getUniqueCities(data)); this.availableCities.set(this.getUniqueCities(data));
}); });
this.socketService.onCandidateRegistered().subscribe(newCandidate => { this.socketService.onCandidateRegistered().subscribe(newCandidate => {
this.applicationList.update(list => [newCandidate, ...list]); this.dataService.cachedApplicationList.update(list => [newCandidate, ...list]);
this.availableCities.set(this.getUniqueCities(this.applicationList())); this.availableCities.set(this.getUniqueCities(this.dataService.cachedApplicationList()));
}); });
this.socketService.onCandidateUpdated().subscribe(updatedCandidate => { this.socketService.onCandidateUpdated().subscribe(updatedCandidate => {
this.applicationList.update(list => this.dataService.cachedApplicationList.update(list =>
list.map(app => app.id === updatedCandidate.id ? updatedCandidate : app) list.map(app => app.id === updatedCandidate.id ? updatedCandidate : app)
); );
this.availableCities.set(this.getUniqueCities(this.applicationList())); this.availableCities.set(this.getUniqueCities(this.dataService.cachedApplicationList()));
}); });
this.socketService.onCandidateDeleted().subscribe(deletedCandidateId => { this.socketService.onCandidateDeleted().subscribe(deletedCandidateId => {
this.applicationList.update(list => this.dataService.cachedApplicationList.update(list =>
list.filter(app => app.id !== deletedCandidateId) list.filter(app => app.id !== deletedCandidateId)
); );
this.availableCities.set(this.getUniqueCities(this.applicationList())); this.availableCities.set(this.getUniqueCities(this.dataService.cachedApplicationList()));
}); });
} }
ngOnDestroy(): void {
this.socketService.disconnect();
}
getUniqueCities(data: any[]): City[] { getUniqueCities(data: any[]): City[] {
const seen: string[] = []; const seen: string[] = [];

View File

@@ -18,7 +18,7 @@
<mat-icon>chevron_left</mat-icon> <mat-icon>chevron_left</mat-icon>
</button> </button>
<span class="navigation-info"> <span class="navigation-info">
{{ currentIndex() + 1 }} of {{ applicationList().length }} {{ currentIndex() + 1 }} of {{ this.dataService.cachedApplicationList().length }}
</span> </span>
<button <button
mat-icon-button mat-icon-button

View File

@@ -1,4 +1,4 @@
import { Component, inject, OnInit, signal, computed } from '@angular/core'; import { Component, inject, OnInit, signal, computed, OnDestroy } from '@angular/core';
import { CandidateDataService } from '../../services/candidate-data.service'; import { CandidateDataService } from '../../services/candidate-data.service';
import { ActivatedRoute, Router, RouterLink, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterLink, RouterModule } from '@angular/router';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@@ -12,6 +12,7 @@ import { SocketIOService } from '../../services/socket-io.service';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { trigger, transition } from '@angular/animations'; import { trigger, transition } from '@angular/animations';
import { slideLeft, slideRight } from './slide.animation'; import { slideLeft, slideRight } from './slide.animation';
import { tap } from 'rxjs';
@Component({ @Component({
selector: 'app-application', selector: 'app-application',
@@ -34,7 +35,7 @@ import { slideLeft, slideRight } from './slide.animation';
]), ]),
], ],
}) })
export class ApplicationComponent implements OnInit { export class ApplicationComponent implements OnInit, OnDestroy {
dataService = inject(CandidateDataService); dataService = inject(CandidateDataService);
socketService = inject(SocketIOService); socketService = inject(SocketIOService);
activatedRoute = inject(ActivatedRoute); activatedRoute = inject(ActivatedRoute);
@@ -43,7 +44,6 @@ export class ApplicationComponent implements OnInit {
currentApplication = signal<any>(null); currentApplication = signal<any>(null);
applicationList = signal<any[]>([]);
currentIndex = signal<number>(-1); currentIndex = signal<number>(-1);
environment = environment; environment = environment;
@@ -55,7 +55,7 @@ export class ApplicationComponent implements OnInit {
canGoToPrevious = computed(() => this.currentIndex() > 0); canGoToPrevious = computed(() => this.currentIndex() > 0);
canGoToNext = computed(() => canGoToNext = computed(() =>
this.currentIndex() >= 0 && this.currentIndex() >= 0 &&
this.currentIndex() < this.applicationList().length - 1 this.currentIndex() < this.dataService.cachedApplicationList().length - 1
); );
canEdit = computed(() => { canEdit = computed(() => {
@@ -68,21 +68,25 @@ export class ApplicationComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
if (this.dataService.cachedApplicationList.length > 0) { this.socketService.connect();
this.applicationList.set(this.dataService.cachedApplicationList); if (this.dataService.cachedApplicationList().length > 0) {
this.initializeApplication(); this.initializeApplication();
} else { 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');
}
});
} }
this.dataService.loadCandidateList().subscribe({
next: (data) => {
this.initializeApplication();
},
error: (error) => {
console.error('Error loading candidate list:', error);
alert('Error loading candidate list');
}
});
}
ngOnDestroy(): void {
this.socketService.disconnect();
} }
editApplication() { editApplication() {
@@ -99,7 +103,7 @@ export class ApplicationComponent implements OnInit {
} }
const applicationId = Number.parseInt(id, 10); const applicationId = Number.parseInt(id, 10);
const foundIndex = this.applicationList().findIndex(app => app.id === applicationId); const foundIndex = this.dataService.cachedApplicationList().findIndex(app => app.id === applicationId);
if (foundIndex === -1) { if (foundIndex === -1) {
alert('Application not found'); alert('Application not found');
@@ -108,7 +112,7 @@ export class ApplicationComponent implements OnInit {
if (this.currentIndex() !== foundIndex) { if (this.currentIndex() !== foundIndex) {
this.currentIndex.set(foundIndex); this.currentIndex.set(foundIndex);
this.loadApplication(applicationId); this.loadApplication(applicationId)?.subscribe();
} }
} }
@@ -118,7 +122,7 @@ export class ApplicationComponent implements OnInit {
} }
this.currentApplication.set(null); this.currentApplication.set(null);
this.dataService.getApplicationDetails(id).subscribe({ return this.dataService.getApplicationDetails(id).pipe(tap({
next: (data) => { next: (data) => {
this.currentApplication.set(data); this.currentApplication.set(data);
const currentRoute = this.router.url; const currentRoute = this.router.url;
@@ -131,15 +135,15 @@ export class ApplicationComponent implements OnInit {
console.error('Error loading application details:', error); console.error('Error loading application details:', error);
alert('Error loading application details'); alert('Error loading application details');
} }
}); }));
} }
goToPrevious() { goToPrevious() {
if (this.canGoToPrevious()) { if (this.canGoToPrevious()) {
const newIndex = this.currentIndex() - 1; const newIndex = this.currentIndex() - 1;
this.currentIndex.set(newIndex); this.currentIndex.set(newIndex);
const prevId = this.applicationList()[newIndex].id; const prevId = this.dataService.cachedApplicationList()[newIndex].id;
this.loadApplication(prevId); this.loadApplication(prevId)?.subscribe();
} }
} }
@@ -147,8 +151,8 @@ export class ApplicationComponent implements OnInit {
if (this.canGoToNext()) { if (this.canGoToNext()) {
const newIndex = this.currentIndex() + 1; const newIndex = this.currentIndex() + 1;
this.currentIndex.set(newIndex); this.currentIndex.set(newIndex);
const nextId = this.applicationList()[newIndex].id; const nextId = this.dataService.cachedApplicationList()[newIndex].id;
this.loadApplication(nextId); this.loadApplication(nextId)?.subscribe();
} }
} }

View File

@@ -1,5 +1,5 @@
<div class="fileUploadWrapper"> <div class="fileUploadWrapper">
<mat-label>Profile Photo</mat-label> <mat-label>Profile Photo*</mat-label>
<div class="fileUploadContainer" [ngStyle]="{ 'margin-top': value ? '5px' : '20px' }"> <div class="fileUploadContainer" [ngStyle]="{ 'margin-top': value ? '5px' : '20px' }">
@@ -24,8 +24,7 @@
<button <button
mat-raised-button mat-raised-button
color="primary" color="primary"
type="button" type="button">
(click)="fileInput.click()">
Browse Browse
</button> </button>
<small style="margin: 10px 0; display: block;">Drag and drop here</small> <small style="margin: 10px 0; display: block;">Drag and drop here</small>

View File

@@ -1,5 +1,5 @@
import { Component, effect, forwardRef, inject, input } from '@angular/core'; import { Component, effect, forwardRef, inject, input, viewChild } from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; import { ControlValueAccessor, FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
@@ -25,13 +25,15 @@ import { CommonModule } from '@angular/common';
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ImageInputComponent), useExisting: forwardRef(() => ImageInputComponent),
multi: true multi: true
} },
], ],
templateUrl: './image-input.component.html', templateUrl: './image-input.component.html',
styleUrl: './image-input.component.scss' styleUrl: './image-input.component.scss'
}) })
export class ImageInputComponent implements ControlValueAccessor { export class ImageInputComponent implements ControlValueAccessor {
fileInput = viewChild<HTMLInputElement>('fileInput');
value: File | null = null; value: File | null = null;
touched = false;
disabled = false; disabled = false;
previewUrl: string | null = null; previewUrl: string | null = null;
src = input<string | null>(null); src = input<string | null>(null);
@@ -54,9 +56,10 @@ export class ImageInputComponent implements ControlValueAccessor {
if (value) { if (value) {
this.previewUrl = URL.createObjectURL(value); this.previewUrl = URL.createObjectURL(value);
} else { } else {
this.previewUrl = null; this.previewUrl = null;
} }
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.onChange = fn; this.onChange = fn;
} }
@@ -71,16 +74,21 @@ export class ImageInputComponent implements ControlValueAccessor {
setFileData(event: Event): void { setFileData(event: Event): void {
this.markAsTouched();
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
if (input?.files?.[0]) { if (input?.files?.[0]) {
this.value = input.files[0]; this.value = input.files[0];
this.previewUrl = URL.createObjectURL(this.value); this.previewUrl = URL.createObjectURL(this.value);
console.log(this.previewUrl)
this.onChange(this.value); this.onChange(this.value);
} }
} }
markAsTouched() {
if (this.touched) return;
this.onTouched();
this.touched = true;
}
clear() { clear() {
this.value = null; this.value = null;
this.isDisplayOnly = false; this.isDisplayOnly = false;

View File

@@ -86,7 +86,6 @@ export class LeafletMapComponent implements AfterViewInit, ControlValueAccessor
if (exact) this.onSelectCity(exact.name); if (exact) this.onSelectCity(exact.name);
else this.onTouched(); else this.onTouched();
console.log(this.marker);
} }

View File

@@ -26,6 +26,9 @@
<!-- Profile Image Upload --> <!-- Profile Image Upload -->
<div class="image-upload"> <div class="image-upload">
<app-image-input [src]="this.previewUrl" formControlName="profileImage"></app-image-input> <app-image-input [src]="this.previewUrl" formControlName="profileImage"></app-image-input>
@if (form.get('profileImage')?.touched && form.get('profileImage')?.hasError('required')) {
<mat-error>Image is required</mat-error>
}
</div> </div>
<!-- Full Name --> <!-- Full Name -->
@@ -43,6 +46,9 @@
<input matInput formControlName="email" /> <input matInput formControlName="email" />
@if (form.get('email')?.hasError('email')) { @if (form.get('email')?.hasError('email')) {
<mat-error>Invalid email</mat-error> <mat-error>Invalid email</mat-error>
}
@if (form.get('email')?.hasError('required')) {
<mat-error>Email is required</mat-error>
} }
</mat-form-field> </mat-form-field>
@@ -105,7 +111,7 @@
</mat-form-field> </mat-form-field>
<!-- Submit Button --> <!-- Submit Button -->
<button mat-raised-button color="accent" type="submit"> <button mat-raised-button color="accent" type="submit" >
{{ editMode() ? 'Update Application' : 'Submit Application' }} {{ editMode() ? 'Update Application' : 'Submit Application' }}
</button> </button>

View File

@@ -51,6 +51,8 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-bottom: 1rem; margin-bottom: 1rem;
align-items: center;
flex-direction: column;
} }
.image-upload img { .image-upload img {
@@ -101,7 +103,6 @@ mat-error {
} }
/* Buttons */ /* Buttons */
button { button {
cursor: pointer; cursor: pointer;

View File

@@ -1,4 +1,4 @@
import { AfterViewInit, Component, ElementRef, inject, input, OnInit, QueryList, signal, ViewChildren } from '@angular/core'; import { AfterViewInit, Component, ElementRef, inject, input, OnDestroy, OnInit, QueryList, Renderer2, signal, ViewChildren } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
@@ -35,15 +35,14 @@ const israeliPhoneRegex = /^(?:(?:(\+?972|\(\+?972\)|\+?\(972\))(?:\s|\.|-)?([1-
templateUrl: './registration.component.html', templateUrl: './registration.component.html',
styleUrls: ['./registration.component.scss'], styleUrls: ['./registration.component.scss'],
}) })
export class RegistrationComponent implements OnInit { export class RegistrationComponent implements OnInit, OnDestroy {
dataService = inject(CandidateDataService); dataService = inject(CandidateDataService);
fb = inject(FormBuilder); fb = inject(FormBuilder);
router = inject(Router); router = inject(Router);
activatedRoute = inject(ActivatedRoute); activatedRoute = inject(ActivatedRoute);
snackBar = inject(MatSnackBar) snackBar = inject(MatSnackBar)
socketService = inject(SocketIOService); socketService = inject(SocketIOService);
renderer = inject(Renderer2);
originalImageUrl: string | null = null; originalImageUrl: string | null = null;
previewUrl: string | null = null; previewUrl: string | null = null;
@@ -63,6 +62,7 @@ export class RegistrationComponent implements OnInit {
}); });
ngOnInit(): void { ngOnInit(): void {
this.socketService.connect();
const idParam = this.activatedRoute.snapshot.paramMap.get('id'); const idParam = this.activatedRoute.snapshot.paramMap.get('id');
const url = this.activatedRoute.snapshot.url.map(s => s.path); const url = this.activatedRoute.snapshot.url.map(s => s.path);
@@ -77,6 +77,10 @@ export class RegistrationComponent implements OnInit {
} }
} }
ngOnDestroy(): void {
this.socketService.disconnect();
}
loadCandidate(id: number) { loadCandidate(id: number) {
this.dataService.getApplicationDetails(id).subscribe({ this.dataService.getApplicationDetails(id).subscribe({
next: (candidate: any) => { next: (candidate: any) => {
@@ -118,6 +122,7 @@ export class RegistrationComponent implements OnInit {
} }
onSubmit() { onSubmit() {
this.form.markAllAsTouched();
if (!this.form.valid) { if (!this.form.valid) {
this.scrollToFirstInvalidField(); this.scrollToFirstInvalidField();
return; return;
@@ -149,21 +154,25 @@ export class RegistrationComponent implements OnInit {
horizontalPosition: 'center', horizontalPosition: 'center',
verticalPosition: 'top', verticalPosition: 'top',
}); });
this.socketService.socket.emit('candidateUpdated', updatedCandidate); this.socketService.socket?.emit('candidateUpdated', updatedCandidate);
this.router.navigate(['/application-list']); this.router.navigate(['/application-list']);
}, },
error: err => alert('Error updating application'), error: err => alert('Error updating application'),
}); });
} else { } else {
this.dataService.submitCandidateForm(formData).subscribe({ this.dataService.submitCandidateForm(formData).subscribe({
next: (newCandidate) => { next: (newCandidate: any) => {
this.snackBar.open('✅ Application saved!', 'Close', { this.snackBar.open('✅ Application saved!', 'Close', {
duration: 5000, duration: 5000,
horizontalPosition: 'center', horizontalPosition: 'center',
verticalPosition: 'top', verticalPosition: 'top',
}); });
this.socketService.socket.emit('candidateRegistered', newCandidate); this.dataService.cachedApplicationList.update((data) => {
this.form.reset(); return { ...data, newCandidate };
})
this.socketService.socket?.emit('candidateRegistered', newCandidate);
this.applicationId.set(newCandidate.id);
this.router.navigate([`/application/${this.applicationId()}`])
}, },
error: err => alert('Error submitting form'), error: err => alert('Error submitting form'),
}); });

View File

@@ -1,16 +1,16 @@
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { inject, Injectable, signal } from "@angular/core"; import { inject, Injectable, signal } from "@angular/core";
import { environment } from "../../environments/environment.development"; import { environment } from "../../environments/environment";
import { delay, tap } from "rxjs"; import { delay, tap } from "rxjs";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class CandidateDataService { export class CandidateDataService {
httpClient = inject(HttpClient) httpClient = inject(HttpClient);
isCandidatesListLoading = signal(false) isCandidatesListLoading = signal(false);
isApplicationDetailsLoading = signal(false) isApplicationDetailsLoading = signal(false);
cachedApplicationList: any[] = [] cachedApplicationList = signal<any[]>([]);
loadCandidateList() { loadCandidateList() {
this.isCandidatesListLoading.set(true) this.isCandidatesListLoading.set(true)
@@ -18,7 +18,7 @@ export class CandidateDataService {
delay(500), delay(500),
tap((data) => { tap((data) => {
this.isCandidatesListLoading.set(false); this.isCandidatesListLoading.set(false);
this.cachedApplicationList = data; this.cachedApplicationList.set(data);
}) })
); );
} }
@@ -45,7 +45,7 @@ export class CandidateDataService {
deleteCandidate(id: number) { deleteCandidate(id: number) {
return this.httpClient.delete(`${environment.hostUrl}/app/candidate/${id}`).pipe( return this.httpClient.delete(`${environment.hostUrl}/app/candidate/${id}`).pipe(
tap(() => { tap(() => {
this.cachedApplicationList = this.cachedApplicationList.filter(c => c.id !== id); this.cachedApplicationList.set(this.cachedApplicationList().filter(c => c.id !== id));
}) })
); );
} }

View File

@@ -1,17 +1,23 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { io } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import { environment } from '../../environments/environment';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class SocketIOService { export class SocketIOService {
socket: Socket | null = null;
connect(){
if(this.socket) return;
this.socket = io(`${environment.socketUrl}`);
}
socket = io('ws://localhost:3000');
onCandidateRegistered(): Observable<any> { onCandidateRegistered(): Observable<any> {
return new Observable(observer => { return new Observable(observer => {
this.socket.on('candidateRegistered', (data) => { this.socket?.on('candidateRegistered', (data) => {
observer.next(data); observer.next(data);
}); });
}); });
@@ -19,7 +25,7 @@ export class SocketIOService {
onCandidateUpdated(): Observable<any> { onCandidateUpdated(): Observable<any> {
return new Observable(observer => { return new Observable(observer => {
this.socket.on('candidateUpdated', (data) => { this.socket?.on('candidateUpdated', (data) => {
observer.next(data); observer.next(data);
}); });
}); });
@@ -27,7 +33,7 @@ export class SocketIOService {
onCandidateDeleted(): Observable<any> { onCandidateDeleted(): Observable<any> {
return new Observable(observer => { return new Observable(observer => {
this.socket.on('candidateDeleted', (data) => { this.socket?.on('candidateDeleted', (data) => {
observer.next(data); observer.next(data);
}); });
}); });
@@ -35,16 +41,16 @@ export class SocketIOService {
onStatsUpdated(): Observable<any> { onStatsUpdated(): Observable<any> {
return new Observable(observer => { return new Observable(observer => {
this.socket.on('statsUpdated', (data) => { this.socket?.on('statsUpdated', (data) => {
observer.next(data); observer.next(data);
}); });
}); });
} }
// disconnect() { disconnect() {
// this.socket.disconnect(); this.socket?.disconnect();
// } }
} }

View File

@@ -1,13 +1,21 @@
import { Injectable, signal, effect, inject } from '@angular/core'; import { Injectable, signal, effect, inject, OnDestroy, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { SocketIOService } from './socket-io.service'; import { SocketIOService } from './socket-io.service';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class StatsService { export class StatsService implements OnInit, OnDestroy {
http = inject(HttpClient); http = inject(HttpClient);
socket = inject(SocketIOService); socket = inject(SocketIOService);
ngOnInit(): void {
this.socket.connect();
}
ngOnDestroy(): void {
this.socket.disconnect();
}
totalVisits = signal(0); totalVisits = signal(0);
totalClicks = signal(0); totalClicks = signal(0);

View File

@@ -1,5 +1,6 @@
export const environment = { export const environment = {
hostUrl: 'http://localhost:3000', hostUrl: 'http://localhost:3000',
mapTilerApiKey: '9LJZ0OppHyT3LzvQW3ce' mapTilerApiKey: '9LJZ0OppHyT3LzvQW3ce',
socketUrl: 'ws://localhost:3000'
}; };

View File

@@ -1,5 +1,5 @@
export const environment = { export const environment = {
hostUrl: 'https://iisa.novikov.click', hostUrl: 'https://iisa.novikov.click',
mapTilerApiKey: '9LJZ0OppHyT3LzvQW3ce' mapTilerApiKey: '9LJZ0OppHyT3LzvQW3ce',
socketUrl: 'https://iisa.novikov.click'
}; };

View File

@@ -1,4 +1,5 @@
export const environment = { export const environment = {
hostUrl: '', hostUrl: '',
mapTilerApiKey:'' mapTilerApiKey: '',
socketUrl: ''
}; };