Compare commits
4 Commits
a053a85dcb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e84e1975c6 | |||
| 12672fa0bf | |||
| 04f8e56d76 | |||
| 64f51967e3 |
66
README.md
66
README.md
@@ -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:
|
||||||
@@ -36,31 +42,3 @@ 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.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,14 +36,13 @@ 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');
|
||||||
@@ -52,7 +51,7 @@ export class ApplicationListComponent implements OnInit {
|
|||||||
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[] = [];
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,13 +68,13 @@ 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({
|
this.dataService.loadCandidateList().subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
this.applicationList.set(data);
|
|
||||||
this.initializeApplication();
|
this.initializeApplication();
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
@@ -82,7 +82,11 @@ export class ApplicationComponent implements OnInit {
|
|||||||
alert('Error loading candidate list');
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -57,6 +59,7 @@ export class ImageInputComponent implements ControlValueAccessor {
|
|||||||
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;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 -->
|
||||||
@@ -44,6 +47,9 @@
|
|||||||
@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>
|
||||||
|
|
||||||
<!-- Phone Number -->
|
<!-- Phone Number -->
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
// }
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
hostUrl: '',
|
hostUrl: '',
|
||||||
mapTilerApiKey:''
|
mapTilerApiKey: '',
|
||||||
|
socketUrl: ''
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user