final fix

This commit is contained in:
2025-08-27 21:02:40 +03:00
parent 6e5b89469f
commit 6880876942
18 changed files with 251 additions and 120 deletions

View File

@@ -1,9 +1,10 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient()]
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(), provideAnimations()]
};

View File

@@ -1,7 +1,7 @@
<div class="container app-list">
<div class="header-row">
<div class="list-header-top">
<button mat-button type="button" routerLink="/landing" class="back-button">
<button mat-button type="button" routerLink="/landing" class="secondary-btn">
Go Back
</button>
<h2 class="page-title">Applications</h2>
@@ -82,7 +82,7 @@
<div class="listItem" [routerLink]="'/application/' + application.id">
@if (application.profileImage) {
<img
[src]="environment.hostUrl + '/uploads/' + application.profileImage"
[src]="application.profileImage"
alt="{{ application.fullName }}"
class="profile-photo"
/>

View File

@@ -18,6 +18,23 @@
position: relative;
margin-bottom: 2rem;
}
button.secondary-btn {
background: transparent;
color: #00ffff;
border: 1px solid #00ffff;
padding: 0.8rem 1.5rem;
border-radius: 8px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
margin-right: 1rem;
}
button.secondary-btn:hover {
background: rgba(0, 255, 255, 0.12);
box-shadow: 0 0 20px #00ffff;
transform: translateY(-2px);
}
.back-button {
position: absolute;

View File

@@ -13,6 +13,7 @@ import { BaseChartDirective } from 'ng2-charts';
import { ChartData, ChartOptions, Chart, registerables } from 'chart.js';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatButtonModule } from '@angular/material/button';
Chart.register(...registerables);
@@ -28,6 +29,7 @@ Chart.register(...registerables);
MatInputModule,
MatProgressSpinnerModule,
BaseChartDirective,
MatButtonModule
],
templateUrl: './application-list.component.html',
styleUrls: ['./application-list.component.scss']

View File

@@ -1,86 +1,95 @@
<div class="container application">
<header class="header">
<header class="header">
<button mat-button type="button" routerLink="/application-list" class="back-button">
<mat-icon>arrow_back</mat-icon>
Go Back
</button>
<h2>🛰 Application Details</h2>
</header>
</header>
<div class="content-wrapper">
<div class="navigation-controls">
<button
mat-icon-button
[disabled]="!canGoToPrevious()"
(click)="goToPrevious()"
class="nav-button"
title="Previous Application"
>
<mat-icon>chevron_left</mat-icon>
</button>
<span class="navigation-info">
{{ currentIndex() + 1 }} of {{ applicationList().length }}
</span>
<button
mat-icon-button
[disabled]="!canGoToNext()"
(click)="goToNext()"
class="nav-button"
title="Next Application"
>
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<div [@animSlider]="currentIndex()" class="application-card-container">
@if (isApplicationDetailsLoading) {
<div class="loading-container">
<mat-spinner diameter="50"></mat-spinner>
<p class="loading-text">Loading application details...</p>
</div>
} @else if (hasApplicationData) {
<div class="content-wrapper">
<div class="navigation-controls">
<button
mat-icon-button
[disabled]="!canGoToPrevious()"
(click)="goToPrevious()"
class="nav-button"
title="Previous Application"
>
<mat-icon>chevron_left</mat-icon>
</button>
<span class="navigation-info">
{{ currentIndex() + 1 }} of {{ applicationList().length }}
</span>
<button
mat-icon-button
[disabled]="!canGoToNext()"
(click)="goToNext()"
class="nav-button"
title="Next Application"
>
<mat-icon>chevron_right</mat-icon>
</button>
<div class="application-card">
<div class="loading-container">
<mat-spinner diameter="50"></mat-spinner>
<p class="loading-text">Loading application details...</p>
</div>
<div class="application-card">
</div>
}
@if(!isApplicationDetailsLoading){
<div class="application-card">
@if(this.currentApplication() === null) {
<div class="no-data">
<mat-icon class="no-data-icon">inbox</mat-icon>
<p>No application data available</p>
<button mat-button routerLink="/application-list" class="back-button">
<mat-icon>arrow_back</mat-icon>
Return to List
</button>
</div>
}
@if (this.currentApplication() !== null) {
<div class="profile-section">
@if (currentApplication().profileImage) {
<div class="profile-image-container">
<img
[src]="environment.hostUrl + '/uploads/' + currentApplication().profileImage"
[src]="currentApplication().profileImage"
[alt]="currentApplication().fullName"
class="profile-image"
/>
</div>
}
<div class="profile-info">
<h3 class="candidate-name">{{ currentApplication().fullName }}</h3>
<div class="basic-info">
<div class="info-item">
<mat-icon>person</mat-icon>
<span>{{ currentApplication().age }} years old</span>
</div>
<div class="info-item">
<mat-icon>location_on</mat-icon>
<span>{{ currentApplication().cityOrRegion }}</span>
</div>
@if (currentApplication().email) {
<div class="info-item">
<mat-icon>person</mat-icon>
<span>{{ currentApplication().age }} years old</span>
<mat-icon>email</mat-icon>
<span>{{ currentApplication().email }}</span>
</div>
}
@if (currentApplication().phoneNumber) {
<div class="info-item">
<mat-icon>location_on</mat-icon>
<span>{{ currentApplication().cityOrRegion }}</span>
<mat-icon>phone</mat-icon>
<span>{{ currentApplication().phoneNumber }}</span>
</div>
@if (currentApplication().email) {
<div class="info-item">
<mat-icon>email</mat-icon>
<span>{{ currentApplication().email }}</span>
</div>
}
@if (currentApplication().phoneNumber) {
<div class="info-item">
<mat-icon>phone</mat-icon>
<span>{{ currentApplication().phoneNumber }}</span>
</div>
}
}
</div>
</div>
</div>
<div class="details-grid">
@if (getHobbiesArray().length > 0) {
<div class="detail-section">
@@ -92,14 +101,12 @@
</div>
</div>
}
@if (currentApplication().justification) {
<div class="detail-section full-width">
<h4><mat-icon>rocket_launch</mat-icon> Journey Justification?</h4>
<p class="detail-content">{{ currentApplication().justification }}</p>
</div>
}
@if (currentApplication().createdAt) {
<div class="detail-section">
<h4><mat-icon>calendar_today</mat-icon> Application Date</h4>
@@ -109,7 +116,6 @@
</div>
}
</div>
<div class="edit-controls">
@if (canEdit()) {
<button
@@ -133,21 +139,14 @@
Delete Application
</button>
</div>
<details class="raw-data-section">
<summary>View Raw Data</summary>
<pre class="raw-data">{{ currentApplication() | json }}</pre>
</details>
</div>
</div>
} @else {
<div class="no-data">
<mat-icon class="no-data-icon">inbox</mat-icon>
<p>No application data available</p>
<button mat-button routerLink="/application-list" class="back-button">
<mat-icon>arrow_back</mat-icon>
Return to List
</button>
}
</div>
}
</div>
</div>

View File

@@ -1,4 +1,6 @@
.container {
:host {
display: block;
width: 100%;
min-height: 100vh;
margin: 0;
@@ -113,17 +115,53 @@
}
}
/* Application Card */
.application-card {
background: rgba(0, 0, 0, 0.4);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(15px);
border: 1px solid rgba(0, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 255, 255, 0.1);
margin-bottom: 2rem;
.delete-button {
margin-left: 12px;
background: rgba(255, 68, 68, 0.1);
border: 1px solid #ff4444;
border-radius: 8px;
color: #ff4444;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
}
.delete-button:hover {
background: rgba(255, 68, 68, 0.2);
color: #ff6666;
box-shadow: 0 0 20px rgba(255, 68, 68, 0.4);
transform: translateY(-2px);
border-color: #ff6666;
}
.delete-button mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
/* Application Card */
.application-card-container {
position: relative;
overflow-x: hidden;
border-radius: 16px;
min-height: 60rem;
.application-card {
position: absolute;
top: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.4);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(15px);
border: 1px solid rgba(0, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 255, 255, 0.1);
}
}
/* Profile Section */
.profile-section {
display: flex;
@@ -414,4 +452,5 @@
font-size: 0.95rem;
font-style: italic;
}
}
}

View File

@@ -10,6 +10,8 @@ import { CommonModule } from '@angular/common';
import { environment } from '../../../environments/environment';
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';
@Component({
selector: 'app-application',
@@ -21,10 +23,16 @@ import { MatSnackBar } from '@angular/material/snack-bar';
MatCardModule,
MatChipsModule,
CommonModule,
RouterLink
RouterLink,
],
templateUrl: './application.component.html',
styleUrls: ['./application.component.scss']
styleUrls: ['./application.component.scss'],
animations: [
trigger('animSlider', [
transition(':increment', slideRight),
transition(':decrement', slideLeft),
]),
],
})
export class ApplicationComponent implements OnInit {
dataService = inject(CandidateDataService);
@@ -43,9 +51,6 @@ export class ApplicationComponent implements OnInit {
return this.dataService.isApplicationDetailsLoading();
}
get hasApplicationData() {
return this.currentApplication() !== null;
}
canGoToPrevious = computed(() => this.currentIndex() > 0);
canGoToNext = computed(() =>

View File

@@ -0,0 +1,45 @@
import { trigger, transition, query, style, animate, group } from '@angular/animations';
export const slideLeft = [
group([
query(':enter',
[
style({ transform: 'translateX(-100%)' }),
animate('.6s ease-out', style({ transform: 'translateX(0%)' }))
],
{
optional: true,
}
),
query(':leave',
[
animate('.6s ease-out', style({ transform: 'translateX(100%)' }))
],
{
optional: true,
}
),
]),
];
export const slideRight = [
group([
query(':enter',
[
style({ transform: 'translateX(100%)' }),
animate('.4s ease-out', style({ transform: 'translateX(0%)' }))
],
{
optional: true,
}
),
query(':leave',
[
animate('.4s ease-out', style({ transform: 'translateX(-100%)' }))
],
{
optional: true,
}
),
]),
];

View File

@@ -3,7 +3,7 @@
<div class="fileUploadContainer" [ngStyle]="{'margin-top': value ? '5px' : '20px'}">
<!-- Image preview -->
@if (value) {
@if (previewUrl) {
<div class="previewContainer">
<img [src]="previewUrl" class="previewImage" />
<button
@@ -17,7 +17,7 @@
}
<!-- Upload UI when no image -->
@if (!value) {
@if (!previewUrl) {
<div class="uploadPrompt">
<mat-icon style="opacity: 60%;">file_upload</mat-icon>
<button mat-raised-button color="primary" type="button" (click)="fileInput.click()">

View File

@@ -1,4 +1,4 @@
import { Component, forwardRef, inject } from '@angular/core';
import { Component, effect, forwardRef, inject, input } from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -34,15 +34,29 @@ export class ImageInputComponent implements ControlValueAccessor {
value: File | null = null;
disabled = false;
previewUrl: string | null = null;
src = input<string | null>(null);
isDisplayOnly = false;
constructor() {
effect(() => {
if (this.src()) {
this.previewUrl = this.src();
}
})
}
onChange = (value: any) => { };
onTouched = () => { };
writeValue(value: File | null): void {
this.value = value;
}
if (value) {
this.previewUrl = URL.createObjectURL(value);
} else {
this.previewUrl = null;
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
@@ -62,12 +76,14 @@ export class ImageInputComponent implements ControlValueAccessor {
this.value = input.files[0];
this.previewUrl = URL.createObjectURL(this.value);
console.log(this.previewUrl)
this.onChange(this.value);
}
}
clear() {
this.value = null;
this.isDisplayOnly = false;
if (this.previewUrl) {
URL.revokeObjectURL(this.previewUrl);
}

View File

@@ -17,7 +17,7 @@
</div>
<div class="stats">
<p>👀 Total Visits: {{ stats.totalVisits() }}</p>
<p>🖱️ Register Button Clicks: {{ stats.totalClicks() }}</p>
<p>Total Visits: {{ stats.totalVisits() }}</p>
<p>Register Button Clicks: {{ stats.totalClicks() }}</p>
</div>
</div>

View File

@@ -7,7 +7,6 @@
margin: 0;
padding: 2rem;
background: radial-gradient(circle at top, #0a0f2c 0%, #000000 100%);
color: #e0e0ff;
font-family: 'Orbitron', Arial, sans-serif;
box-sizing: border-box;
display: flex;

View File

@@ -3,9 +3,9 @@
<div class="container">
<div class="registration-header">
@if(editMode()){
<button mat-button type="button" routerLink="/application-list" class="button">
Cancel Editing
</button>
<button mat-button type="button" (click)="cancelEdit()" class="button">
Cancel Editing
</button>
}@else {
<button mat-button type="button" routerLink="/landing" class="button">
Go Back
@@ -22,10 +22,12 @@
<form class="registration-form" [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- Profile Image Upload -->
<div class="image-upload">
<app-image-input formControlName="profileImage"></app-image-input>
</div>
<div class="image-upload">
<app-image-input [src]="this.previewUrl" formControlName="profileImage"></app-image-input>
</div>
<!-- Full Name -->
<mat-form-field appearance="outline">
<mat-label>Full Name</mat-label>

View File

@@ -44,8 +44,9 @@ export class RegistrationComponent implements OnInit {
activatedRoute = inject(ActivatedRoute);
snackBar = inject(MatSnackBar)
socketService = inject(SocketIOService);
previewUrl: string | ArrayBuffer | null = null;
originalImageUrl: string | null = null;
previewUrl: string | null = null;
editMode = signal(false);
applicationId = signal<number | null>(null);
@@ -65,10 +66,11 @@ export class RegistrationComponent implements OnInit {
const idParam = this.activatedRoute.snapshot.paramMap.get('id');
const url = this.activatedRoute.snapshot.url.map(s => s.path);
if (url.includes('edit') && idParam) {
if (url.includes('edit') && idParam !== null) {
const id = Number.parseInt(idParam);
this.editMode.set(true);
this.applicationId.set(+idParam);
this.loadCandidate(+idParam);
this.applicationId.set(id);
this.loadCandidate(id);
} else {
this.editMode.set(false);
this.applicationId.set(null);
@@ -87,9 +89,9 @@ export class RegistrationComponent implements OnInit {
hobbies: candidate.hobbies,
justification: candidate.justification,
});
if (candidate.profileImageUrl) {
this.previewUrl = candidate.profileImageUrl;
if (candidate.profileImage) {
this.originalImageUrl = candidate.profileImage;
this.previewUrl = candidate.profileImage;
this.form.get('profileImage')?.clearValidators();
this.form.get('profileImage')?.updateValueAndValidity();
}
@@ -150,7 +152,7 @@ export class RegistrationComponent implements OnInit {
this.socketService.socket.emit('candidateUpdated', updatedCandidate);
this.router.navigate(['/application-list']);
},
error: err => console.error('Error updating application', err),
error: err => alert('Error updating application'),
});
} else {
this.dataService.submitCandidateForm(formData).subscribe({
@@ -163,10 +165,13 @@ export class RegistrationComponent implements OnInit {
this.socketService.socket.emit('candidateRegistered', newCandidate);
this.form.reset();
},
error: err => console.error('Error submitting form', err),
error: err => alert('Error submitting form'),
});
}
}
cancelEdit() {
this.previewUrl = this.originalImageUrl;
this.router.navigate(['/application-list']);
}
}