This commit is contained in:
2025-08-28 17:07:20 +03:00
parent 6880876942
commit 5105aad107
9 changed files with 276 additions and 123 deletions

View File

@@ -56,7 +56,6 @@ ng e2e
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources ## Additional Resources
## TODO
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. 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.
@@ -64,9 +63,4 @@ For more information on using the Angular CLI, including detailed command refere
-centralize scss check the same styles for differnet buttons
****animations transitions (prev next cards)
--DEPLOY AND WRITE README

View File

@@ -1,104 +1,140 @@
<div class="container app-list"> <div class="container app-list">
<!-- Top Header -->
<div class="header-row"> <div class="header-row">
<div class="list-header-top"> <div class="list-header-top">
<button mat-button type="button" routerLink="/landing" class="secondary-btn"> <button
mat-button
type="button"
routerLink="/landing"
class="secondary-btn">
Go Back Go Back
</button> </button>
<h2 class="page-title">Applications</h2> <h2 class="page-title">Applications</h2>
</div> </div>
@if (!isLoading) { <!-- View Toggle -->
<div class="charts-row"> <div class="view-toggle">
<div class="chart-container"> <button
<h3>Age Distribution</h3> mat-button
<canvas baseChart [class.active]="viewMode() === 'map'"
[data]="ageChartData()" (click)="viewMode.set('map')">
[options]="chartOptions" Map
[type]="'pie'"> </button>
</canvas> <button
</div> mat-button
[class.active]="viewMode() === 'charts'"
<div class="chart-container"> (click)="viewMode.set('charts')">
<h3>City Distribution</h3> Charts
<canvas baseChart </button>
[data]="cityChartData()" </div>
[options]="chartOptions"
[type]="'pie'">
</canvas>
</div>
</div>
}
@if (!isLoading) {
<div class="list-header">
<mat-form-field appearance="outline" class="search-field">
<mat-label>Search</mat-label>
<input
matInput
[ngModel]="searchTerm()"
(ngModelChange)="searchTerm.set($event)"
placeholder="Type to search..."
/>
</mat-form-field>
<mat-form-field appearance="outline" class="filter-field">
<mat-label>City</mat-label>
<mat-select
[ngModel]="filterCity()"
(ngModelChange)="filterCity.set($event)"
>
<mat-option value="">All</mat-option>
@for (city of availableCities(); track city.name) {
<mat-option [value]="city.name">{{ city.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="sort-field">
<mat-label>Sort by</mat-label>
<mat-select
[ngModel]="sortField()"
(ngModelChange)="sortField.set($event)"
>
@for (option of sortFields; track option.value) {
<mat-option [value]="option.value">
{{ option.viewValue }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
</div> </div>
<!-- Map View -->
@if (viewMode() === 'map') {
<app-candidates-map
[candidates]="applicationList()"
[cities]="availableCities()">
</app-candidates-map>
}
<!-- Charts View -->
@if (!isLoading && viewMode() === 'charts') {
<div class="charts-row">
<div class="chart-container">
<h3>Age Distribution</h3>
<canvas baseChart
[data]="ageChartData()"
[options]="chartOptions"
[type]="'pie'">
</canvas>
</div>
<div class="chart-container">
<h3>City Distribution</h3>
<canvas baseChart
[data]="cityChartData()"
[options]="chartOptions"
[type]="'pie'">
</canvas>
</div>
</div>
}
<!-- Search / Filters -->
@if (!isLoading) {
<div class="list-header">
<mat-form-field appearance="outline" class="search-field">
<mat-label>Search</mat-label>
<input
matInput
[ngModel]="searchTerm()"
(ngModelChange)="searchTerm.set($event)"
placeholder="Type to search..." />
</mat-form-field>
<mat-form-field appearance="outline" class="filter-field">
<mat-label>City</mat-label>
<mat-select
[ngModel]="filterCity()"
(ngModelChange)="filterCity.set($event)">
<mat-option value="">All</mat-option>
@for (city of availableCities(); track city.name) {
<mat-option [value]="city.name">
{{ city.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="sort-field">
<mat-label>Sort by</mat-label>
<mat-select
[ngModel]="sortField()"
(ngModelChange)="sortField.set($event)">
@for (option of sortFields; track option.value) {
<mat-option [value]="option.value">
{{ option.viewValue }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
<!-- Loading State -->
@if (isLoading) { @if (isLoading) {
<div class="loading-container"> <div class="loading-container">
<mat-spinner diameter="50"></mat-spinner> <mat-spinner diameter="50"></mat-spinner>
<p class="loading-text">Loading applications...</p> <p class="loading-text">Loading applications...</p>
</div> </div>
} @else { }
@else {
<!-- Applications List -->
<div class="list"> <div class="list">
@for (application of sortedList(); track application.id) { @for (application of sortedList(); track application.id) {
<div class="listItem" [routerLink]="'/application/' + application.id"> <div
class="listItem"
[routerLink]="'/application/' + application.id">
@if (application.profileImage) { @if (application.profileImage) {
<img <img
[src]="application.profileImage" [src]="application.profileImage"
alt="{{ application.fullName }}" alt="{{ application.fullName }}"
class="profile-photo" class="profile-photo" />
/>
} }
<div class="info"> <div class="info">
<p class="app-name">{{ application.fullName }}</p> <p class="app-name">{{ application.fullName }}</p>
<p>Age: {{ application.age }}</p> <p>Age: {{ application.age }}</p>
<p>City: {{ application.cityOrRegion }}</p> <p>City: {{ application.cityOrRegion }}</p>
</div> </div>
<button
<button
mat-icon-button mat-icon-button
color="warn" color="warn"
class="delete-button" class="delete-button"
(click)="onDeleteClick($event ,application.id)" (click)="onDeleteClick($event, application.id)"
[attr.aria-label]="'Delete ' + application.fullName" [attr.aria-label]="'Delete ' + application.fullName">
>
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</div> </div>

View File

@@ -229,3 +229,34 @@ button.secondary-btn:hover {
0% { background-position: -200% center; } 0% { background-position: -200% center; }
100% { background-position: 200% center; } 100% { background-position: 200% center; }
} }
.view-toggle {
display: flex;
justify-content: center;
gap: 1rem;
margin: 1rem 0 2rem;
button {
background: transparent;
border: 1px solid #00ffff;
color: #00ffff;
padding: 0.6rem 1.2rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
&:hover {
background: rgba(0, 255, 255, 0.12);
box-shadow: 0 0 12px #00ffff;
}
&.active {
background: rgba(0, 255, 255, 0.15);
box-shadow: 0 0 18px #00ffff;
transform: translateY(-2px);
}
}
}

View File

@@ -14,6 +14,7 @@ import { ChartData, ChartOptions, Chart, registerables } from 'chart.js';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { CandidatesMapComponent } from '../candidates-map/candidates-map.component';
Chart.register(...registerables); Chart.register(...registerables);
@@ -29,7 +30,8 @@ Chart.register(...registerables);
MatInputModule, MatInputModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
BaseChartDirective, BaseChartDirective,
MatButtonModule MatButtonModule,
CandidatesMapComponent,
], ],
templateUrl: './application-list.component.html', templateUrl: './application-list.component.html',
styleUrls: ['./application-list.component.scss'] styleUrls: ['./application-list.component.scss']
@@ -45,6 +47,7 @@ export class ApplicationListComponent implements OnInit {
searchTerm = signal(''); searchTerm = signal('');
filterCity = signal(''); filterCity = signal('');
sortField = signal<string>('fullName'); sortField = signal<string>('fullName');
viewMode = signal<'map' | 'charts'>('map');
availableCities = signal<City[]>(CITY_LIST); availableCities = signal<City[]>(CITY_LIST);

View File

@@ -0,0 +1,11 @@
.map-container {
height: 400px;
width: 90%;
max-width: 900px;
margin: 2rem auto;
border: 2px solid #00ffff;
border-radius: 14px;
box-shadow: 0 0 20px rgba(0, 255, 255, 0.4);
background: rgba(0, 0, 0, 0.3);
overflow: hidden;
}

View File

@@ -0,0 +1,71 @@
import { Component, AfterViewInit, Input, ElementRef, ViewChild, OnChanges, SimpleChanges, input } from '@angular/core';
import * as L from 'leaflet';
import { CommonModule } from '@angular/common';
import { City } from '../../shared/cities';
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
});
@Component({
selector: 'app-candidates-map',
standalone: true,
imports: [CommonModule],
template: `<div class="map-container" #mapEl></div>`,
styleUrls: ['./candidates-map.component.scss']
})
export class CandidatesMapComponent implements AfterViewInit, OnChanges {
@ViewChild('mapEl', { static: true }) mapEl!: ElementRef<HTMLDivElement>;
candidates = input<any[]>([]);
cities = input<City[]>([]);
map!: L.Map;
markersLayer = L.layerGroup();
ngAfterViewInit(): void {
this.map = L.map(this.mapEl.nativeElement).setView([31.7683, 35.2137], 7);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap contributors',
}).addTo(this.map);
this.markersLayer.addTo(this.map);
this.renderMarkers();
}
ngOnChanges(changes: SimpleChanges): void {
if ((changes['candidates'] || changes['cities']) && this.map) {
this.renderMarkers();
}
}
renderMarkers(): void {
this.markersLayer.clearLayers();
if (!this.map || !this.candidates()?.length) return;
const candidatesPerCity: Record<string, number> = {};
this.candidates().forEach(c => {
if (c.cityOrRegion) {
candidatesPerCity[c.cityOrRegion] = (candidatesPerCity[c.cityOrRegion] || 0) + 1;
}
});
this.cities().forEach(city => {
const count = candidatesPerCity[city.name];
if (!count) return;
const marker = L.marker([city.lat, city.lng]);
marker.bindTooltip(`${city.name}: ${count} candidate(s)`, {
permanent: false,
direction: 'top'
});
marker.addTo(this.markersLayer);
});
}
}

View File

@@ -1,7 +1,8 @@
<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' }">
<!-- Image preview --> <!-- Image preview -->
@if (previewUrl) { @if (previewUrl) {
<div class="previewContainer"> <div class="previewContainer">
@@ -16,18 +17,22 @@
</div> </div>
} }
<!-- Upload UI when no image --> <!-- Upload UI when no image -->
@if (!previewUrl) { @if (!previewUrl) {
<div class="uploadPrompt"> <div class="uploadPrompt">
<mat-icon style="opacity: 60%;">file_upload</mat-icon> <mat-icon style="opacity: 0.6;">file_upload</mat-icon>
<button mat-raised-button color="primary" type="button" (click)="fileInput.click()"> <button
mat-raised-button
color="primary"
type="button"
(click)="fileInput.click()">
Browse Browse
</button> </button>
<small style="margin: 10px; display:block;">Drag and drop here</small> <small style="margin: 10px 0; display: block;">Drag and drop here</small>
</div> </div>
} }
<!-- Hidden file input --> <!-- Hidden file input -->
<input <input
#fileInput #fileInput
class="fileInput" class="fileInput"

View File

@@ -1,33 +1,33 @@
<div class="container"> <div class="container">
<div class="registration-header"> <div class="registration-header">
@if(editMode()){
<button mat-button type="button" (click)="cancelEdit()" class="button"> <!-- Header buttons -->
Cancel Editing @if(editMode()) {
</button> <button mat-button type="button" (click)="cancelEdit()" class="button">
}@else { Cancel Editing
</button>
} @else {
<button mat-button type="button" routerLink="/landing" class="button"> <button mat-button type="button" routerLink="/landing" class="button">
Go Back Go Back
</button> </button>
} }
@if(editMode()){
<h2>Spaceflight Candidate Registration Update</h2> <!-- Header title -->
} @if(editMode()) {
@else { <h2>Spaceflight Candidate Registration Update</h2>
<h2>Spaceflight Candidate Registration</h2> } @else {
} <h2>Spaceflight Candidate Registration</h2>
</div> }
</div>
<form class="registration-form" [formGroup]="form" (ngSubmit)="onSubmit()"> <form class="registration-form" [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- 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> </div>
</div>
<!-- Full Name --> <!-- Full Name -->
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Full Name</mat-label> <mat-label>Full Name</mat-label>
@@ -68,21 +68,21 @@
</mat-form-field> </mat-form-field>
<!-- City --> <!-- City -->
<app-leaflet-map formControlName="cityOrRegion" class="app-leaflet-map"></app-leaflet-map>
<app-leaflet-map
formControlName="cityOrRegion"
class="app-leaflet-map">
</app-leaflet-map>
@if (form.get('cityOrRegion')?.invalid && (form.get('cityOrRegion')?.touched || form.get('cityOrRegion')?.dirty)) { @if (form.get('cityOrRegion')?.invalid && (form.get('cityOrRegion')?.touched || form.get('cityOrRegion')?.dirty)) {
@if (form.get('cityOrRegion')?.hasError('required')) { @if (form.get('cityOrRegion')?.hasError('required')) {
<mat-error style="margin-bottom: 8px; padding: 0 16px;">City selection is required</mat-error> <mat-error style="margin-bottom: 8px; padding: 0 16px;">
City selection is required
</mat-error>
}
@if (form.get('cityOrRegion')?.hasError('invalidCity')) {
<mat-error style="margin-bottom: 8px; padding: 0 16px;">
Please select a valid city from the list
</mat-error>
}
} }
@if (form.get('cityOrRegion')?.hasError('invalidCity')) {
<mat-error style="margin-bottom: 8px; padding: 0 16px;">Please select a valid city from the list</mat-error>
}
}
<!-- Hobbies --> <!-- Hobbies -->
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Hobbies</mat-label> <mat-label>Hobbies</mat-label>
@@ -104,12 +104,14 @@
} }
</mat-form-field> </mat-form-field>
<!-- Submit --> <!-- 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>
</form> </form>
<!-- Footer -->
<div class="footer"> <div class="footer">
🚀 Powered by <a href="#">Israeli Imaginary Space Agency</a> | © 2025 🚀 Powered by <a href="#">Israeli Imaginary Space Agency</a> | © 2025
</div> </div>