Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions otpcasestudy-angular-app/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete this file as there is no use as of now

}).compileComponents();
});

it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});

it(`should have the 'otp-admin' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('otp-admin');
});

it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, otp-admin');
});
});
12 changes: 10 additions & 2 deletions otpcasestudy-angular-app/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@ import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AuthInterceptor } from './services/auth-interceptor.service';
import { HTTP_INTERCEPTORS } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync(), provideHttpClient()]
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideAnimationsAsync(),
provideHttpClient(withInterceptorsFromDi()),
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
]
};
27 changes: 23 additions & 4 deletions otpcasestudy-angular-app/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,41 @@ import { ScheduleExamComponent } from './components/admin/schedule-exam/schedule
import { UploadQuestionsComponent } from './components/admin/upload-questions/upload-questions.component';
import { ViewCandidatesComponent } from './components/admin/view-candidates/view-candidates.component';
import { DownloadResultsComponent } from './components/admin/download-results/download-results.component';
import { DownloadAbsentCandidatesComponent } from './components/admin/download-absent-candidates/download-absent-candidates.component';
import { AdminAuthGuard } from './guards/admin-auth-guard';
import { LoginGuard } from './guards/login.guard';
import { LandingComponent } from './components/candidate/landing/landing.component';
import { AdminLoginComponent } from './components/candidate/admin-login/admin-login.component';
import { EmailVerificationComponent } from './components/candidate/email-verification/email-verification.component';
import { RegistrationComponent } from './components/candidate/registration/registration.component';
import { ExamWindowComponent } from './components/exam-window/exam-window.component';
import { ExamExitGuard } from './guards/exam-exit.guard';
import { CandidateLoginComponent } from './components/candidate/candidate-login/candidate-login.component';

export const routes: Routes = [
{
path: 'admin',
canActivate: [AdminAuthGuard],
component: AdminLayoutComponent,
children: [
{ path: 'dashboard', component: DashboardComponent },
{ path: 'schedule-exam', component: ScheduleExamComponent },
{ path: 'upload-questions', component: UploadQuestionsComponent },
{ path: 'view-candidates', component: ViewCandidatesComponent },
{ path: 'download-results', component: DownloadResultsComponent },
{ path: 'absent-candidates', component: DownloadAbsentCandidatesComponent },
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' }
]
},
{
path: 'login',
canActivate: [LoginGuard],
loadComponent: () => import('./components/admin/login/login.component').then(m => m.LoginComponent)
},
{ path: 'landing', component: LandingComponent },
{ path: 'admin-login', component: AdminLoginComponent },
{ path: 'verify-email', component: EmailVerificationComponent },
{ path: 'registration', component: RegistrationComponent },
{ path: 'exam', component: ExamWindowComponent, canDeactivate: [ExamExitGuard] },
{ path: 'candidate-login', component: CandidateLoginComponent },
{ path: '', pathMatch: 'full', redirectTo: 'admin/dashboard' },
{ path: '**', redirectTo: 'admin/dashboard' }
];
{ path: '**', redirectTo: 'landing' }
];
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

.admin-sidenav {
width: 280px;
// background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: #3b1877;
color: white;
border: none;
Expand Down Expand Up @@ -47,12 +46,12 @@

&:hover {
background-color: rgba(255, 255, 255, 0.1);
color: white;
color: #fff;
}

&.active-link {
background-color: rgba(255, 255, 255, 0.2);
color: white;
background-color: rgba(255, 255, 255, 0.7);
color: #3b1877;
font-weight: 500;
}

Expand Down Expand Up @@ -97,7 +96,7 @@
.toolbar-title {
font-size: 1.25rem;
font-weight: 500;
display: none; // Hidden on small screens
display: none;
}
}

Expand Down Expand Up @@ -150,11 +149,7 @@
}

@media (min-width: 768px) {
.admin-toolbar {
.toolbar-brand {
.toolbar-title {
display: inline-block;
}
}
.admin-toolbar .toolbar-brand .toolbar-title {
display: inline-block;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatBadgeModule } from '@angular/material/badge';
import { MatMenuModule } from '@angular/material/menu';
import { MatDividerModule } from '@angular/material/divider';
import { AuthAdminService } from '../../../services/auth-admin.service';

@Component({
selector: 'app-admin-layout',
Expand Down Expand Up @@ -64,14 +65,14 @@ export class AdminLayoutComponent {
}
];

constructor(private router: Router) { }
constructor(private router: Router, private authAdminService: AuthAdminService) { }

toggleSidenav() {
this.sidenavOpened = !this.sidenavOpened;
}

logout() {
localStorage.removeItem('otp_admin_logged_in');
this.router.navigate(['/login']);
this.authAdminService.logout();
this.router.navigate(['/admin-login']);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,21 +94,27 @@ export class DashboardComponent implements OnInit, AfterViewInit {
},
error: (err) => {
console.error(err);
this.snackBar.open('Failed to load exams', '', { duration: 5000 });
this.snackBar.open(err?.error?.message || 'Failed to load exams', '', { duration: 5000 });
this.loading = false;
}
});

// Fetch active exam count
this.examService.getActiveExamCount().subscribe({
next: (count) => this.activeExams = count,
error: (err) => console.error(err)
error: (err) => {
console.error(err)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove console

this.snackBar.open(err?.error?.message || 'Failed to load Active Exam Count', '', { duration: 5000 });
}
});

// Fetch total active candidates
this.examService.getTotalActiveCandidates().subscribe({
next: (count) => this.totalCandidates = count,
error: (err) => console.error(err)
error: (err) => {
console.error(err);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove console

this.snackBar.open(err?.error?.message || 'Failed to load Active Candidates', '', { duration: 5000 });
}
})
}

Expand All @@ -129,12 +135,12 @@ export class DashboardComponent implements OnInit, AfterViewInit {
}

getStatusColor(status: string): string {
const statusColorMap: { [key: string]: string } = {
upcoming: 'primary',
ongoing: 'accent',
finished: 'warn',
};
return statusColorMap[status] || 'default';
switch (status) {
case 'upcoming': return 'primary';
case 'ongoing': return 'accent';
case 'ended': return 'warn';
default: return 'default';
}
}

getStatusText(status: string): string {
Expand All @@ -156,10 +162,6 @@ export class DashboardComponent implements OnInit, AfterViewInit {
return 'ended';
}

// viewResults(exam: Exam): void {
// this.snackBar.open(`Viewing results for ${exam.title}`, 'Close', { duration: 2000 });
// }

downloadResults(exam: Exam): void {
this.examService.downloadResults(exam.id).subscribe({
next: (blob) => {
Expand All @@ -174,7 +176,7 @@ export class DashboardComponent implements OnInit, AfterViewInit {
},
error: (err) => {
console.error(err);
this.snackBar.open('Failed to download results.', '', { duration: 5000 });
this.snackBar.open(err?.error?.message || 'Failed to download results.', '', { duration: 5000 });
}
});
}
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export class DownloadResultsComponent implements OnInit {
next: (exams) => {
this.exams = exams;
},
error: () => {
this.snackBar.open('Failed to load completed exams.', '', { duration: 5000 });
error: (err) => {
this.snackBar.open(err?.error?.message || 'Failed to load completed exams.', '', { duration: 5000 });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ } should be use if there are multiple linse, on above only single line present so use instead error: (err) => this.snackBar.open(err?.error?.message || 'Failed to load completed exams.', '', { duration: 5000 });

}
});
}
Expand All @@ -57,8 +57,8 @@ export class DownloadResultsComponent implements OnInit {
this.snackBar.open('Results downloaded!', '', { duration: 5000 });
this.downloadingExamId = null;
},
error: () => {
this.snackBar.open('Failed to download results.', '', { duration: 5000 });
error: (err) => {
this.snackBar.open(err?.error?.message || 'Failed to download results.', '', { duration: 5000 });
this.downloadingExamId = null;
}
});
Expand All @@ -70,7 +70,6 @@ export class DownloadResultsComponent implements OnInit {
a.href = url;
a.download = filename;
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,53 @@
<p>login works!</p>
<div class="login-container">
<div class="branding-panel">
<div class="branding-content">
<mat-icon class="brand-logo">school</mat-icon>
<h1 class="brand-title">NextStep Admin Portal</h1>
<p class="brand-subtitle">Online Test Platform Management</p>
</div>
</div>
<div class="login-panel">
<mat-card class="login-card">
<mat-card-header>
<mat-card-title>
<h2>Admin Login</h2>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<mat-form-field appearance="outline" class="form-field">
<mat-label>Username</mat-label>
<input
matInput
formControlName="username"
required
placeholder="Enter your username"
/>
<mat-icon matSuffix>person</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>Password</mat-label>
<input
matInput
type="password"
formControlName="password"
required
placeholder="Enter your password"
/>
<mat-icon matSuffix>lock</mat-icon>
</mat-form-field>
<button
mat-raised-button
color="primary"
class="login-button"
type="submit"
[disabled]="loginForm.invalid || loading"
>
<mat-spinner *ngIf="loading" diameter="20"></mat-spinner>
<span *ngIf="!loading">Login</span>
</button>
</form>
</mat-card-content>
</mat-card>
</div>
</div>
Loading