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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# dependencies
/node_modules
/.pnp
.angular/
.pnp.js

# testing
Expand All @@ -23,3 +24,5 @@ yarn-debug.log*
yarn-error.log*

.idea/
.angular/
.angular/
33 changes: 31 additions & 2 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
import { Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { AuthorizedGuard } from './auth/guards/authorized.guard';
import { NotAuthorizedGuard } from './auth/guards/not-authorized.guard';

export const routes: Routes = [
/* Add your code here */
{
path: 'login',
loadChildren: () => import('./login/login.module').then(m => m.LoginModule),
canActivate: [NotAuthorizedGuard]
},
{
path: 'registration',
loadChildren: () => import('./registration/registration.module').then(m => m.RegistrationModule),
canActivate: [NotAuthorizedGuard]
},
{
path: 'courses',
loadChildren: () => import('./store/courses/courses.module').then(m => m.CoursesModule),
canLoad: [AuthorizedGuard]
},
{
path: '',
redirectTo: 'courses',
pathMatch: 'full'
},
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
71 changes: 55 additions & 16 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,23 +1,62 @@
<app-header></app-header>
<div class="app-root">
<!-- Search component -->
<!--<app-search></app-search>-->
<app-header>
<span>Harry Potter</span>
<app-button [buttonText]="'LOGOUT'"></app-button>
</app-header>

<!-- Info component -->
<!--<app-info></app-info>-->
<div class="button-container">
<app-button [buttonText]="'SHOW COURSE'"></app-button>
<app-button [iconName]="'delete'"></app-button>
<app-button [iconName]="'edit'"></app-button>
</div>

<!-- Course Card component -->
<!--<app-course-card></app-course-card>-->
<div class="app-root">
<div *ngIf="selectedCourse" class="course-info-container">
<div class="course-info-header">
<h2>{{ selectedCourse.title }}</h2>
</div>
<div class="course-info-content">
<div class="course-info-description">
<p><b>Description:</b></p>
<p>{{ selectedCourse.description }}</p>
</div>
<div class="course-info-details">
<p><b>ID:</b> {{ selectedCourse.id }}</p>
<p><b>Duration:</b> {{ selectedCourse.duration }} min</p>
<p><b>Created:</b> {{ selectedCourse.creationDate | date }}</p>
<p><b>Authors:</b> {{ selectedCourse.authors.join(", ") }}</p>
</div>
</div>
<div class="course-info-actions">
<app-button [buttonText]="'BACK'" (click)="onBackClick()"></app-button>
</div>
</div>

<!-- Login Form component -->
<!--<app-login-form></app-login-form>-->
<!-- <app-registration-form></app-registration-form>-->
<!-- <app-login-form></app-login-form>-->

<!-- Login Form component -->
<!--<app-registration-form></app-registration-form>-->
<div *ngIf="!selectedCourse">
<app-info
[title]="'Your List Is Empty'"
[text]="'Please use Add New Course button to add your first course'"
>
<app-button [buttonText]="'ADD NEW COURSE'"></app-button>
</app-info>

<!-- Login Form component -->
<!--<app-course-form></app-course-form>-->
<app-course-card
*ngFor="let course of courses"
[title]="course.title"
[description]="course.description"
[creationDate]="course.creationDate"
[duration]="course.duration"
[authors]="course.authors"
[editable]="true"
(showCourse)="showCourse(course.id)"
>
<app-button [buttonText]="'SHOW COURSE'"></app-button>
<app-button [iconName]="'edit'"></app-button>
<app-button [iconName]="'delete'"></app-button>
</app-course-card>
</div>

<!-- Modal component -->
<!-- <app-modal [message]="'Message'" [title]="'Title'" [okButtonText]="'Ok'" [cancelButtonText]="'Cancel'"></app-modal> -->

</div>
21 changes: 21 additions & 0 deletions src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,24 @@
justify-content: center;
padding: 24px;
}

.button-container {
display: flex;
gap: 1rem;
}

.course-info-container {
padding: 32px;
background-color: #ffffff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 4px;
}

.course-info-header {
margin-bottom: 24px;
}

.course-info-header h2 {
font-size: 24px;
font-weight: bold;
}
48 changes: 48 additions & 0 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,58 @@
import { Component } from '@angular/core';

interface Course {
id: string;
title: string;
description: string;
creationDate: Date;
duration: number;
authors: string[];
}

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'courses-app';
creationDate: Date = new Date('2012-03-20');
courses: Course[] = [
{
id: '1',
title: 'Angular',
description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
creationDate: new Date('2012-03-20'),
duration: 150,
authors: ['Dave Haisenberg', 'Tony Ja']
},
{
id: '2',
title: 'Java',
description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
creationDate: new Date('2017-08-14'),
duration: 90,
authors: ['Dave Simmonds', 'Valentina Lary']
},
{
id: '3',
title: 'ASP .NET',
description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
creationDate: new Date('2022-06-01'),
duration: 210,
authors: ['Sam Smith', 'Tony Robbins']
}
];

selectedCourse: Course | null = null;

showCourse(courseId: string) {
this.selectedCourse = this.courses.find(course => course.id === courseId) || null;
}

onBackClick() {
this.selectedCourse = null;
}
}


21 changes: 18 additions & 3 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,35 @@ import { BrowserModule } from '@angular/platform-browser';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { SharedModule } from '@shared/shared.module';
import { AppComponent } from '@app/app.component';
import { CourseInfoComponent } from '@features/course-info/course-info.component';
//import { CourseInfoComponent } from '@features/course-info/course-info.component';
import { NotAuthorizedGuard } from '@app/auth/guards/not-authorized.guard';
import { AuthorizedGuard } from '@app/auth/guards/authorized.guard';
import { CoursesStoreService } from '@app/services/courses-store.service';
import { CoursesService } from '@app/services/courses.service';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { reducers, effects } from '@app/store';

@NgModule({
declarations: [AppComponent, CourseInfoComponent],
declarations: [AppComponent,
//CourseInfoComponent
],
imports: [
BrowserModule,
SharedModule,
FontAwesomeModule,
FormsModule,
HttpClientModule,
AppRoutingModule,
ReactiveFormsModule,
StoreModule.forRoot(reducers),
EffectsModule.forRoot(effects)
],
providers: [AuthorizedGuard, NotAuthorizedGuard, CoursesService, CoursesStoreService],
bootstrap: [AppComponent],
})
export class AppModule {}
export class AppModule {}
43 changes: 40 additions & 3 deletions src/app/auth/interceptors/token.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,44 @@
import { Injectable } from '@angular/core';
import { HttpInterceptor } from '@angular/common/http';
import { Injectable } from "@angular/core";
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse,
} from "@angular/common/http";
import { Observable, throwError } from "rxjs";
import { catchError } from "rxjs/operators";
import { AuthService } from "../services/auth.service";
import { SessionStorageService } from "../services/session-storage.service";

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
// Add your code here
constructor(
private sessionStorageService: SessionStorageService,
private authService: AuthService
) {}

intercept(
request: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
const token = this.sessionStorageService.getToken();

if (token) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
}

return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
this.authService.logout();
}
return throwError(() => error);
})
);
}
}
64 changes: 52 additions & 12 deletions src/app/auth/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,70 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { SessionStorageService } from './session-storage.service';

interface UserCredentials { email: string; password: string; }
interface RegisterData extends UserCredentials { name: string; }
interface AuthResponse { token: string; }

@Injectable({
providedIn: 'root'
})
export class AuthService {
login(user: any) { // replace 'any' with the required interface
// Add your code here

private apiUrl = 'http://localhost:4000/api';

private isAuthorized$$ = new BehaviorSubject<boolean>(!!this.sessionStorageService.getToken());

public isAuthorized$: Observable<boolean> = this.isAuthorized$$.asObservable();

constructor(
private http: HttpClient,
private sessionStorageService: SessionStorageService,
private router: Router
) { }

login(credentials: UserCredentials): Observable<AuthResponse> {
return this.http.post<AuthResponse>(`${this.apiUrl}/auth/login`, credentials).pipe(
tap((response: AuthResponse) => {
this.sessionStorageService.setToken(response.token);
this.isAuthorized$$.next(true);
}),
catchError(error => {
return throwError(() => error);
})
);
}

logout() {
// Add your code here
logout(): void {
this.sessionStorageService.deleteToken();
this.isAuthorized$$.next(false);
this.router.navigate(['/login']);
}

register(user: any) { // replace 'any' with the required interface
// Add your code here
register(userData: RegisterData): Observable<AuthResponse> {
return this.http.post<AuthResponse>(`${this.apiUrl}/auth/register`, userData).pipe(
tap((response: AuthResponse) => {
this.sessionStorageService.setToken(response.token);
this.isAuthorized$$.next(true);
}),
catchError(error => {
return throwError(() => error);
})
);
}

get isAuthorised() {
// Add your code here. Get isAuthorized$$ value
get isAuthorized(): boolean {
return this.isAuthorized$$.value;
}

set isAuthorised(value: boolean) {
// Add your code here. Change isAuthorized$$ value
set isAuthorized(value: boolean) {
this.isAuthorized$$.next(value);
}

getLoginUrl() {
// Add your code here
getLoginUrl(): string {
return '/login';
}
}
Loading