mirror of
https://github.com/richard-loafle/fuse-angular.git
synced 2025-01-10 04:25:08 +00:00
Merge remote-tracking branch 'origin/demo' into starter
This commit is contained in:
parent
ad2b19a07a
commit
fa0d74504b
|
@ -1 +0,0 @@
|
||||||
<router-outlet></router-outlet>
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector : 'academy',
|
|
||||||
templateUrl : './academy.component.html',
|
|
||||||
encapsulation : ViewEncapsulation.None,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class AcademyComponent
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
constructor()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
||||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
||||||
import { FuseFindByKeyPipeModule } from '@fuse/pipes/find-by-key';
|
|
||||||
import { SharedModule } from 'app/shared/shared.module';
|
|
||||||
import { academyRoutes } from 'app/modules/admin/apps/academy/academy.routing';
|
|
||||||
import { AcademyComponent } from 'app/modules/admin/apps/academy/academy.component';
|
|
||||||
import { AcademyDetailsComponent } from 'app/modules/admin/apps/academy/details/details.component';
|
|
||||||
import { AcademyListComponent } from 'app/modules/admin/apps/academy/list/list.component';
|
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
AcademyComponent,
|
|
||||||
AcademyDetailsComponent,
|
|
||||||
AcademyListComponent
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
RouterModule.forChild(academyRoutes),
|
|
||||||
MatButtonModule,
|
|
||||||
MatFormFieldModule,
|
|
||||||
MatIconModule,
|
|
||||||
MatInputModule,
|
|
||||||
MatProgressBarModule,
|
|
||||||
MatSelectModule,
|
|
||||||
MatSidenavModule,
|
|
||||||
MatSlideToggleModule,
|
|
||||||
MatTooltipModule,
|
|
||||||
FuseFindByKeyPipeModule,
|
|
||||||
SharedModule,
|
|
||||||
MatTabsModule
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class AcademyModule
|
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
|
|
||||||
import { Observable, throwError } from 'rxjs';
|
|
||||||
import { catchError } from 'rxjs/operators';
|
|
||||||
import { Category, Course } from 'app/modules/admin/apps/academy/academy.types';
|
|
||||||
import { AcademyService } from 'app/modules/admin/apps/academy/academy.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AcademyCategoriesResolver implements Resolve<any>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
constructor(private _academyService: AcademyService)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ Public methods
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolver
|
|
||||||
*
|
|
||||||
* @param route
|
|
||||||
* @param state
|
|
||||||
*/
|
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Category[]>
|
|
||||||
{
|
|
||||||
return this._academyService.getCategories();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AcademyCoursesResolver implements Resolve<any>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
constructor(private _academyService: AcademyService)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ Public methods
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolver
|
|
||||||
*
|
|
||||||
* @param route
|
|
||||||
* @param state
|
|
||||||
*/
|
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Course[]>
|
|
||||||
{
|
|
||||||
return this._academyService.getCourses();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AcademyCourseResolver implements Resolve<any>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
private _router: Router,
|
|
||||||
private _academyService: AcademyService
|
|
||||||
)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ Public methods
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolver
|
|
||||||
*
|
|
||||||
* @param route
|
|
||||||
* @param state
|
|
||||||
*/
|
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Course>
|
|
||||||
{
|
|
||||||
return this._academyService.getCourseById(route.paramMap.get('id'))
|
|
||||||
.pipe(
|
|
||||||
// Error here means the requested task is not available
|
|
||||||
catchError((error) => {
|
|
||||||
|
|
||||||
// Log the error
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
// Get the parent url
|
|
||||||
const parentUrl = state.url.split('/').slice(0, -1).join('/');
|
|
||||||
|
|
||||||
// Navigate to there
|
|
||||||
this._router.navigateByUrl(parentUrl);
|
|
||||||
|
|
||||||
// Throw an error
|
|
||||||
return throwError(error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { Route } from '@angular/router';
|
|
||||||
import { AcademyComponent } from 'app/modules/admin/apps/academy/academy.component';
|
|
||||||
import { AcademyListComponent } from 'app/modules/admin/apps/academy/list/list.component';
|
|
||||||
import { AcademyDetailsComponent } from 'app/modules/admin/apps/academy/details/details.component';
|
|
||||||
import { AcademyCategoriesResolver, AcademyCourseResolver, AcademyCoursesResolver } from 'app/modules/admin/apps/academy/academy.resolvers';
|
|
||||||
|
|
||||||
export const academyRoutes: Route[] = [
|
|
||||||
{
|
|
||||||
path : '',
|
|
||||||
component: AcademyComponent,
|
|
||||||
resolve : {
|
|
||||||
categories: AcademyCategoriesResolver
|
|
||||||
},
|
|
||||||
children : [
|
|
||||||
{
|
|
||||||
path : '',
|
|
||||||
pathMatch: 'full',
|
|
||||||
component: AcademyListComponent,
|
|
||||||
resolve : {
|
|
||||||
courses: AcademyCoursesResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path : ':id',
|
|
||||||
component: AcademyDetailsComponent,
|
|
||||||
resolve : {
|
|
||||||
course: AcademyCourseResolver
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
|
@ -1,91 +0,0 @@
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
|
||||||
import { tap } from 'rxjs/operators';
|
|
||||||
import { Category, Course } from 'app/modules/admin/apps/academy/academy.types';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AcademyService
|
|
||||||
{
|
|
||||||
// Private
|
|
||||||
private _categories: BehaviorSubject<Category[] | null> = new BehaviorSubject(null);
|
|
||||||
private _course: BehaviorSubject<Course | null> = new BehaviorSubject(null);
|
|
||||||
private _courses: BehaviorSubject<Course[] | null> = new BehaviorSubject(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
constructor(private _httpClient: HttpClient)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ Accessors
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Getter for categories
|
|
||||||
*/
|
|
||||||
get categories$(): Observable<Category[]>
|
|
||||||
{
|
|
||||||
return this._categories.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Getter for courses
|
|
||||||
*/
|
|
||||||
get courses$(): Observable<Course[]>
|
|
||||||
{
|
|
||||||
return this._courses.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Getter for course
|
|
||||||
*/
|
|
||||||
get course$(): Observable<Course>
|
|
||||||
{
|
|
||||||
return this._course.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ Public methods
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get categories
|
|
||||||
*/
|
|
||||||
getCategories(): Observable<Category[]>
|
|
||||||
{
|
|
||||||
return this._httpClient.get<Category[]>('api/apps/academy/categories').pipe(
|
|
||||||
tap((response: any) => {
|
|
||||||
this._categories.next(response);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get courses
|
|
||||||
*/
|
|
||||||
getCourses(): Observable<Course[]>
|
|
||||||
{
|
|
||||||
return this._httpClient.get<Course[]>('api/apps/academy/courses').pipe(
|
|
||||||
tap((response: any) => {
|
|
||||||
this._courses.next(response);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get course by id
|
|
||||||
*/
|
|
||||||
getCourseById(id: string): Observable<Course>
|
|
||||||
{
|
|
||||||
return this._httpClient.get<Course>('api/apps/academy/courses/course', {params: {id}}).pipe(
|
|
||||||
tap((response: any) => {
|
|
||||||
this._course.next(response);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
export interface Category
|
|
||||||
{
|
|
||||||
id?: string;
|
|
||||||
title?: string;
|
|
||||||
slug?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Course
|
|
||||||
{
|
|
||||||
id?: string;
|
|
||||||
title?: string;
|
|
||||||
slug?: string;
|
|
||||||
description?: string;
|
|
||||||
category?: string;
|
|
||||||
duration?: number;
|
|
||||||
steps?: {
|
|
||||||
order?: number;
|
|
||||||
title?: string;
|
|
||||||
subtitle?: string;
|
|
||||||
content?: string;
|
|
||||||
}[];
|
|
||||||
totalSteps?: number;
|
|
||||||
updatedAt?: number;
|
|
||||||
featured?: boolean;
|
|
||||||
progress?: {
|
|
||||||
currentStep?: number;
|
|
||||||
completed?: number;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,200 +0,0 @@
|
||||||
<div class="absolute inset-0 flex flex-col min-w-0 overflow-hidden">
|
|
||||||
|
|
||||||
<mat-drawer-container class="flex-auto h-full">
|
|
||||||
|
|
||||||
<!-- Drawer -->
|
|
||||||
<mat-drawer
|
|
||||||
class="w-90 dark:bg-gray-900"
|
|
||||||
[autoFocus]="false"
|
|
||||||
[mode]="drawerMode"
|
|
||||||
[opened]="drawerOpened"
|
|
||||||
#matDrawer>
|
|
||||||
<div class="flex flex-col items-start p-8 border-b">
|
|
||||||
<!-- Back to courses -->
|
|
||||||
<a
|
|
||||||
class="inline-flex items-center leading-6 text-primary hover:underline"
|
|
||||||
[routerLink]="['..']">
|
|
||||||
<span class="inline-flex items-center">
|
|
||||||
<mat-icon
|
|
||||||
class="icon-size-5 text-current"
|
|
||||||
[svgIcon]="'heroicons_solid:arrow-sm-left'"></mat-icon>
|
|
||||||
<span class="ml-1.5 font-medium leading-5">Back to courses</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<!-- Course category -->
|
|
||||||
<ng-container *ngIf="(course.category | fuseFindByKey:'slug':categories) as category">
|
|
||||||
<div
|
|
||||||
class="mt-7 py-0.5 px-3 rounded-full text-sm font-semibold"
|
|
||||||
[ngClass]="{'text-blue-800 bg-blue-100 dark:text-blue-50 dark:bg-blue-500': category.slug === 'web',
|
|
||||||
'text-green-800 bg-green-100 dark:text-green-50 dark:bg-green-500': category.slug === 'android',
|
|
||||||
'text-pink-800 bg-pink-100 dark:text-pink-50 dark:bg-pink-500': category.slug === 'cloud',
|
|
||||||
'text-amber-800 bg-amber-100 dark:text-amber-50 dark:bg-amber-500': category.slug === 'firebase'}">
|
|
||||||
{{category.title}}
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<!-- Course title & description -->
|
|
||||||
<div class="mt-3 text-2xl font-semibold">{{course.title}}</div>
|
|
||||||
<div class="text-secondary">{{course.description}}</div>
|
|
||||||
<!-- Course time -->
|
|
||||||
<div class="mt-6 flex items-center leading-5 text-md text-secondary">
|
|
||||||
<mat-icon
|
|
||||||
class="icon-size-5 text-hint"
|
|
||||||
[svgIcon]="'heroicons_solid:clock'"></mat-icon>
|
|
||||||
<div class="ml-1.5">{{course.duration}} minutes</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Steps -->
|
|
||||||
<div class="py-2 px-8">
|
|
||||||
<ol>
|
|
||||||
<ng-container *ngFor="let step of course.steps; let last = last">
|
|
||||||
<li class="relative group py-6"
|
|
||||||
[class.current-step]="step.order === currentStep">
|
|
||||||
<ng-container *ngIf="!last">
|
|
||||||
<div
|
|
||||||
class="absolute top-6 left-4 w-0.5 h-full -ml-px"
|
|
||||||
[ngClass]="{'bg-primary': step.order < currentStep,
|
|
||||||
'bg-gray-300 dark:bg-gray-600': step.order >= currentStep}"></div>
|
|
||||||
</ng-container>
|
|
||||||
<div
|
|
||||||
class="relative flex items-start cursor-pointer"
|
|
||||||
(click)="goToStep(step.order)">
|
|
||||||
<div
|
|
||||||
class="flex flex-0 items-center justify-center w-8 h-8 rounded-full ring-2 ring-inset ring-transparent bg-card dark:bg-default"
|
|
||||||
[ngClass]="{'bg-primary dark:bg-primary text-on-primary group-hover:bg-primary-800': step.order < currentStep,
|
|
||||||
'ring-primary': step.order === currentStep,
|
|
||||||
'ring-gray-300 dark:ring-gray-600 group-hover:ring-gray-400': step.order > currentStep}">
|
|
||||||
<!-- Check icon, show if the step is completed -->
|
|
||||||
<ng-container *ngIf="step.order < currentStep">
|
|
||||||
<mat-icon
|
|
||||||
class="icon-size-5 text-current"
|
|
||||||
[svgIcon]="'heroicons_solid:check'"></mat-icon>
|
|
||||||
</ng-container>
|
|
||||||
<!-- Step order, show if the step is the current step -->
|
|
||||||
<ng-container *ngIf="step.order === currentStep">
|
|
||||||
<div class="text-md font-semibold text-primary dark:text-primary-500">{{step.order + 1}}</div>
|
|
||||||
</ng-container>
|
|
||||||
<!-- Step order, show if the step is not completed -->
|
|
||||||
<ng-container *ngIf="step.order > currentStep">
|
|
||||||
<div class="text-md font-semibold text-hint group-hover:text-secondary">{{step.order + 1}}</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<div class="font-medium leading-4">{{step.title}}</div>
|
|
||||||
<div class="mt-1.5 text-md leading-4 text-secondary">{{step.subtitle}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ng-container>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</mat-drawer>
|
|
||||||
|
|
||||||
<!-- Drawer content -->
|
|
||||||
<mat-drawer-content class="flex flex-col overflow-hidden">
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="lg:hidden flex flex-0 items-center py-2 pl-4 pr-6 sm:py-4 md:pl-6 md:pr-8 border-b lg:border-b-0 bg-card dark:bg-transparent">
|
|
||||||
<!-- Title & Actions -->
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
[routerLink]="['..']">
|
|
||||||
<mat-icon [svgIcon]="'heroicons_outline:arrow-sm-left'"></mat-icon>
|
|
||||||
</button>
|
|
||||||
<h2 class="ml-2.5 text-md sm:text-xl font-medium tracking-tight truncate">
|
|
||||||
{{course.title}}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<mat-progress-bar
|
|
||||||
class="hidden lg:block flex-0 h-0.5 w-full"
|
|
||||||
[value]="100 * (currentStep + 1) / course.totalSteps"></mat-progress-bar>
|
|
||||||
|
|
||||||
<!-- Main -->
|
|
||||||
<div
|
|
||||||
class="flex-auto overflow-y-auto"
|
|
||||||
cdkScrollable>
|
|
||||||
|
|
||||||
<!-- Steps -->
|
|
||||||
<mat-tab-group
|
|
||||||
class="fuse-mat-no-header"
|
|
||||||
[animationDuration]="'200'"
|
|
||||||
#courseSteps>
|
|
||||||
<ng-container *ngFor="let step of course.steps">
|
|
||||||
<mat-tab>
|
|
||||||
<ng-template matTabContent>
|
|
||||||
<div
|
|
||||||
class="prose prose-sm max-w-3xl mx-auto sm:my-2 lg:mt-4 p-6 sm:p-10 sm:py-12 rounded-2xl shadow overflow-hidden bg-card"
|
|
||||||
[innerHTML]="step.content"></div>
|
|
||||||
</ng-template>
|
|
||||||
</mat-tab>
|
|
||||||
</ng-container>
|
|
||||||
</mat-tab-group>
|
|
||||||
|
|
||||||
<!-- Navigation - Desktop -->
|
|
||||||
<div class="z-10 sticky hidden lg:flex bottom-4 p-4">
|
|
||||||
<div class="flex items-center justify-center mx-auto p-2 rounded-full shadow-lg bg-primary">
|
|
||||||
<button
|
|
||||||
class="flex-0"
|
|
||||||
mat-flat-button
|
|
||||||
[color]="'primary'"
|
|
||||||
(click)="goToPreviousStep()">
|
|
||||||
<mat-icon
|
|
||||||
class="mr-2"
|
|
||||||
[svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
|
|
||||||
<span class="mr-1">Prev</span>
|
|
||||||
</button>
|
|
||||||
<div class="flex items-center justify-center mx-2.5 font-medium leading-5 text-on-primary">
|
|
||||||
<span>{{currentStep + 1}}</span>
|
|
||||||
<span class="mx-0.5 text-hint">/</span>
|
|
||||||
<span>{{course.totalSteps}}</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="flex-0"
|
|
||||||
mat-flat-button
|
|
||||||
[color]="'primary'"
|
|
||||||
(click)="goToNextStep()">
|
|
||||||
<span class="ml-1">Next</span>
|
|
||||||
<mat-icon
|
|
||||||
class="ml-2"
|
|
||||||
[svgIcon]="'heroicons_outline:arrow-narrow-right'"></mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Progress & Navigation - Mobile -->
|
|
||||||
<div class="lg:hidden flex items-center p-4 border-t bg-card">
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
(click)="matDrawer.toggle()">
|
|
||||||
<mat-icon [svgIcon]="'heroicons_outline:view-list'"></mat-icon>
|
|
||||||
</button>
|
|
||||||
<div class="flex items-center justify-center ml-1 lg:ml-2 font-medium leading-5">
|
|
||||||
<span>{{currentStep + 1}}</span>
|
|
||||||
<span class="mx-0.5 text-hint">/</span>
|
|
||||||
<span>{{course.totalSteps}}</span>
|
|
||||||
</div>
|
|
||||||
<mat-progress-bar
|
|
||||||
class="flex-auto ml-6 rounded-full"
|
|
||||||
[value]="100 * (currentStep + 1) / course.totalSteps"></mat-progress-bar>
|
|
||||||
<button
|
|
||||||
class="ml-4"
|
|
||||||
mat-icon-button
|
|
||||||
(click)="goToPreviousStep()">
|
|
||||||
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="ml-0.5"
|
|
||||||
mat-icon-button
|
|
||||||
(click)="goToNextStep()">
|
|
||||||
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-right'"></mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</mat-drawer-content>
|
|
||||||
|
|
||||||
</mat-drawer-container>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -1,204 +0,0 @@
|
||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
|
||||||
import { DOCUMENT } from '@angular/common';
|
|
||||||
import { MatTabGroup } from '@angular/material/tabs';
|
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
import { takeUntil } from 'rxjs/operators';
|
|
||||||
import { FuseMediaWatcherService } from '@fuse/services/media-watcher';
|
|
||||||
import { Category, Course } from 'app/modules/admin/apps/academy/academy.types';
|
|
||||||
import { AcademyService } from 'app/modules/admin/apps/academy/academy.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector : 'academy-details',
|
|
||||||
templateUrl : './details.component.html',
|
|
||||||
encapsulation : ViewEncapsulation.None,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class AcademyDetailsComponent implements OnInit, OnDestroy
|
|
||||||
{
|
|
||||||
@ViewChild('courseSteps', {static: true}) courseSteps: MatTabGroup;
|
|
||||||
categories: Category[];
|
|
||||||
course: Course;
|
|
||||||
currentStep: number = 0;
|
|
||||||
drawerMode: 'over' | 'side' = 'side';
|
|
||||||
drawerOpened: boolean = true;
|
|
||||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
@Inject(DOCUMENT) private _document: Document,
|
|
||||||
private _academyService: AcademyService,
|
|
||||||
private _changeDetectorRef: ChangeDetectorRef,
|
|
||||||
private _elementRef: ElementRef,
|
|
||||||
private _fuseMediaWatcherService: FuseMediaWatcherService
|
|
||||||
)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ Lifecycle hooks
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On init
|
|
||||||
*/
|
|
||||||
ngOnInit(): void
|
|
||||||
{
|
|
||||||
// Get the categories
|
|
||||||
this._academyService.categories$
|
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
|
||||||
.subscribe((categories: Category[]) => {
|
|
||||||
|
|
||||||
// Get the categories
|
|
||||||
this.categories = categories;
|
|
||||||
|
|
||||||
// Mark for check
|
|
||||||
this._changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the course
|
|
||||||
this._academyService.course$
|
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
|
||||||
.subscribe((course: Course) => {
|
|
||||||
|
|
||||||
// Get the course
|
|
||||||
this.course = course;
|
|
||||||
|
|
||||||
// Go to step
|
|
||||||
this.goToStep(course.progress.currentStep);
|
|
||||||
|
|
||||||
// Mark for check
|
|
||||||
this._changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Subscribe to media changes
|
|
||||||
this._fuseMediaWatcherService.onMediaChange$
|
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
|
||||||
.subscribe(({matchingAliases}) => {
|
|
||||||
|
|
||||||
// Set the drawerMode and drawerOpened
|
|
||||||
if ( matchingAliases.includes('lg') )
|
|
||||||
{
|
|
||||||
this.drawerMode = 'side';
|
|
||||||
this.drawerOpened = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.drawerMode = 'over';
|
|
||||||
this.drawerOpened = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark for check
|
|
||||||
this._changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On destroy
|
|
||||||
*/
|
|
||||||
ngOnDestroy(): void
|
|
||||||
{
|
|
||||||
// Unsubscribe from all subscriptions
|
|
||||||
this._unsubscribeAll.next();
|
|
||||||
this._unsubscribeAll.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ Public methods
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go to given step
|
|
||||||
*
|
|
||||||
* @param step
|
|
||||||
*/
|
|
||||||
goToStep(step: number): void
|
|
||||||
{
|
|
||||||
// Set the current step
|
|
||||||
this.currentStep = step;
|
|
||||||
|
|
||||||
// Go to the step
|
|
||||||
this.courseSteps.selectedIndex = this.currentStep;
|
|
||||||
|
|
||||||
// Mark for check
|
|
||||||
this._changeDetectorRef.markForCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go to previous step
|
|
||||||
*/
|
|
||||||
goToPreviousStep(): void
|
|
||||||
{
|
|
||||||
// Return if we already on the first step
|
|
||||||
if ( this.currentStep === 0 )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go to step
|
|
||||||
this.goToStep(this.currentStep - 1);
|
|
||||||
|
|
||||||
// Scroll the current step selector from sidenav into view
|
|
||||||
this._scrollCurrentStepElementIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go to next step
|
|
||||||
*/
|
|
||||||
goToNextStep(): void
|
|
||||||
{
|
|
||||||
// Return if we already on the last step
|
|
||||||
if ( this.currentStep === this.course.totalSteps - 1 )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go to step
|
|
||||||
this.goToStep(this.currentStep + 1);
|
|
||||||
|
|
||||||
// Scroll the current step selector from sidenav into view
|
|
||||||
this._scrollCurrentStepElementIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Track by function for ngFor loops
|
|
||||||
*
|
|
||||||
* @param index
|
|
||||||
* @param item
|
|
||||||
*/
|
|
||||||
trackByFn(index: number, item: any): any
|
|
||||||
{
|
|
||||||
return item.id || index;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ Private methods
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scrolls the current step element from
|
|
||||||
* sidenav into the view. This only happens when
|
|
||||||
* previous/next buttons pressed as we don't want
|
|
||||||
* to change the scroll position of the sidebar
|
|
||||||
* when the user actually clicks around the sidebar.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private _scrollCurrentStepElementIntoView(): void
|
|
||||||
{
|
|
||||||
// Wrap everything into setTimeout so we can make sure that the 'current-step' class points to correct element
|
|
||||||
setTimeout(() => {
|
|
||||||
|
|
||||||
// Get the current step element and scroll it into view
|
|
||||||
const currentStepElement = this._document.getElementsByClassName('current-step')[0];
|
|
||||||
if ( currentStepElement )
|
|
||||||
{
|
|
||||||
currentStepElement.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block : 'start'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,196 +0,0 @@
|
||||||
<div
|
|
||||||
class="absolute inset-0 flex flex-col min-w-0 overflow-y-auto"
|
|
||||||
cdkScrollable>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="relative flex-0 py-8 px-4 sm:p-16 overflow-hidden bg-gray-800 dark">
|
|
||||||
<!-- Background - @formatter:off -->
|
|
||||||
<!-- Rings -->
|
|
||||||
<svg class="absolute inset-0 pointer-events-none"
|
|
||||||
viewBox="0 0 960 540" width="100%" height="100%" preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
|
|
||||||
<circle r="234" cx="196" cy="23"></circle>
|
|
||||||
<circle r="234" cx="790" cy="491"></circle>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<!-- @formatter:on -->
|
|
||||||
<div class="z-10 relative flex flex-col items-center">
|
|
||||||
<h2 class="text-xl font-semibold">FUSE ACADEMY</h2>
|
|
||||||
<div class="mt-1 text-4xl sm:text-7xl font-extrabold tracking-tight leading-tight text-center">
|
|
||||||
What do you want to learn today?
|
|
||||||
</div>
|
|
||||||
<div class="max-w-2xl mt-6 sm:text-2xl text-center tracking-tight text-secondary">
|
|
||||||
Our courses will step you through the process of a building small applications, or adding new features to existing applications.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main -->
|
|
||||||
<div class="flex flex-auto p-6 sm:p-10">
|
|
||||||
|
|
||||||
<div class="flex flex-col flex-auto w-full max-w-xs sm:max-w-5xl mx-auto">
|
|
||||||
<!-- Filters -->
|
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-between w-full max-w-xs sm:max-w-none">
|
|
||||||
<mat-form-field class="fuse-mat-no-subscript w-full sm:w-36">
|
|
||||||
<mat-select
|
|
||||||
[value]="'all'"
|
|
||||||
(selectionChange)="filterByCategory($event)">
|
|
||||||
<mat-option [value]="'all'">All</mat-option>
|
|
||||||
<ng-container *ngFor="let category of categories">
|
|
||||||
<mat-option [value]="category.slug">{{category.title}}</mat-option>
|
|
||||||
</ng-container>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field
|
|
||||||
class="fuse-mat-no-subscript w-full sm:w-72 mt-4 sm:mt-0 sm:ml-4"
|
|
||||||
[floatLabel]="'always'">
|
|
||||||
<mat-icon
|
|
||||||
matPrefix
|
|
||||||
class="icon-size-5"
|
|
||||||
[svgIcon]="'heroicons_solid:search'"></mat-icon>
|
|
||||||
<input
|
|
||||||
(input)="filterByQuery(query.value)"
|
|
||||||
placeholder="Search by title or description"
|
|
||||||
matInput
|
|
||||||
#query>
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-slide-toggle
|
|
||||||
class="mt-8 sm:mt-0 sm:ml-auto"
|
|
||||||
[color]="'primary'"
|
|
||||||
(change)="toggleCompleted($event)">
|
|
||||||
Hide completed
|
|
||||||
</mat-slide-toggle>
|
|
||||||
</div>
|
|
||||||
<!-- Courses -->
|
|
||||||
<ng-container *ngIf="this.filteredCourses.length; else noCourses">
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 mt-8 sm:mt-10">
|
|
||||||
<ng-container *ngFor="let course of filteredCourses">
|
|
||||||
<!-- Course -->
|
|
||||||
<div class="flex flex-col h-96 shadow rounded-2xl overflow-hidden bg-card">
|
|
||||||
<div class="flex flex-col p-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<!-- Course category -->
|
|
||||||
<ng-container *ngIf="(course.category | fuseFindByKey:'slug':categories) as category">
|
|
||||||
<div
|
|
||||||
class="py-0.5 px-3 rounded-full text-sm font-semibold"
|
|
||||||
[ngClass]="{'text-blue-800 bg-blue-100 dark:text-blue-50 dark:bg-blue-500': category.slug === 'web',
|
|
||||||
'text-green-800 bg-green-100 dark:text-green-50 dark:bg-green-500': category.slug === 'android',
|
|
||||||
'text-pink-800 bg-pink-100 dark:text-pink-50 dark:bg-pink-500': category.slug === 'cloud',
|
|
||||||
'text-amber-800 bg-amber-100 dark:text-amber-50 dark:bg-amber-500': category.slug === 'firebase'}">
|
|
||||||
{{category.title}}
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<!-- Completed at least once -->
|
|
||||||
<div class="flex items-center">
|
|
||||||
<ng-container *ngIf="course.progress.completed > 0">
|
|
||||||
<mat-icon
|
|
||||||
class="icon-size-5 text-green-600"
|
|
||||||
[svgIcon]="'heroicons_solid:badge-check'"
|
|
||||||
[matTooltip]="'You completed this course at least once'"></mat-icon>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Course title & description -->
|
|
||||||
<div class="mt-4 text-lg font-medium">{{course.title}}</div>
|
|
||||||
<div class="mt-0.5 line-clamp-2 text-secondary">{{course.description}}</div>
|
|
||||||
<div class="w-12 h-1 my-6 border-t-2"></div>
|
|
||||||
<!-- Course time -->
|
|
||||||
<div class="flex items-center leading-5 text-md text-secondary">
|
|
||||||
<mat-icon
|
|
||||||
class="icon-size-5 text-hint"
|
|
||||||
[svgIcon]="'heroicons_solid:clock'"></mat-icon>
|
|
||||||
<div class="ml-1.5">{{course.duration}} minutes</div>
|
|
||||||
</div>
|
|
||||||
<!-- Course completion -->
|
|
||||||
<div class="flex items-center mt-2 leading-5 text-md text-secondary">
|
|
||||||
<mat-icon
|
|
||||||
class="icon-size-5 text-hint"
|
|
||||||
[svgIcon]="'heroicons_solid:academic-cap'"></mat-icon>
|
|
||||||
<ng-container *ngIf="course.progress.completed === 0">
|
|
||||||
<div class="ml-1.5">Never completed</div>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="course.progress.completed > 0">
|
|
||||||
<div class="ml-1.5">
|
|
||||||
<span>Completed</span>
|
|
||||||
<span class="ml-1">
|
|
||||||
<!-- Once -->
|
|
||||||
<ng-container *ngIf="course.progress.completed === 1">once</ng-container>
|
|
||||||
<!-- Twice -->
|
|
||||||
<ng-container *ngIf="course.progress.completed === 2">twice</ng-container>
|
|
||||||
<!-- Others -->
|
|
||||||
<ng-container *ngIf="course.progress.completed > 2">{{course.progress.completed}}
|
|
||||||
{{course.progress.completed | i18nPlural: {
|
|
||||||
'=0' : 'time',
|
|
||||||
'=1' : 'time',
|
|
||||||
'other': 'times'
|
|
||||||
} }}
|
|
||||||
</ng-container>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Footer -->
|
|
||||||
<div class="flex flex-col w-full mt-auto">
|
|
||||||
<!-- Course progress -->
|
|
||||||
<div class="relative h-0.5">
|
|
||||||
<div
|
|
||||||
class="z-10 absolute inset-x-0 h-6 -mt-3"
|
|
||||||
[matTooltip]="course.progress.currentStep / course.totalSteps | percent"
|
|
||||||
[matTooltipPosition]="'above'"
|
|
||||||
[matTooltipClass]="'-mb-0.5'"></div>
|
|
||||||
<mat-progress-bar
|
|
||||||
class="h-0.5"
|
|
||||||
[value]="(100 * course.progress.currentStep) / course.totalSteps"></mat-progress-bar>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Course launch button -->
|
|
||||||
<div class="px-6 py-4 text-right bg-gray-50 dark:bg-transparent">
|
|
||||||
<button
|
|
||||||
mat-stroked-button
|
|
||||||
[routerLink]="[course.id]">
|
|
||||||
<span class="inline-flex items-center">
|
|
||||||
|
|
||||||
<!-- Not started -->
|
|
||||||
<ng-container *ngIf="course.progress.currentStep === 0">
|
|
||||||
<!-- Never completed -->
|
|
||||||
<ng-container *ngIf="course.progress.completed === 0">
|
|
||||||
<span>Start</span>
|
|
||||||
</ng-container>
|
|
||||||
<!-- Completed before -->
|
|
||||||
<ng-container *ngIf="course.progress.completed > 0">
|
|
||||||
<span>Start again</span>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Started -->
|
|
||||||
<ng-container *ngIf="course.progress.currentStep > 0">
|
|
||||||
<span>Continue</span>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<mat-icon
|
|
||||||
class="ml-1.5 icon-size-5"
|
|
||||||
[svgIcon]="'heroicons_solid:arrow-sm-right'"></mat-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- No courses -->
|
|
||||||
<ng-template #noCourses>
|
|
||||||
<div class="flex flex-auto flex-col items-center justify-center bg-gray-100 dark:bg-transparent">
|
|
||||||
<mat-icon
|
|
||||||
class="icon-size-20"
|
|
||||||
[svgIcon]="'iconsmind:file_search'"></mat-icon>
|
|
||||||
<div class="mt-6 text-2xl font-semibold tracking-tight text-secondary">No courses found!</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -1,159 +0,0 @@
|
||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { MatSelectChange } from '@angular/material/select';
|
|
||||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
|
||||||
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
|
|
||||||
import { takeUntil } from 'rxjs/operators';
|
|
||||||
import { AcademyService } from 'app/modules/admin/apps/academy/academy.service';
|
|
||||||
import { Category, Course } from 'app/modules/admin/apps/academy/academy.types';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector : 'academy-list',
|
|
||||||
templateUrl : './list.component.html',
|
|
||||||
encapsulation : ViewEncapsulation.None,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class AcademyListComponent implements OnInit, OnDestroy
|
|
||||||
{
|
|
||||||
categories: Category[];
|
|
||||||
courses: Course[];
|
|
||||||
filteredCourses: Course[];
|
|
||||||
filters: {
|
|
||||||
categorySlug$: BehaviorSubject<string>;
|
|
||||||
query$: BehaviorSubject<string>;
|
|
||||||
hideCompleted$: BehaviorSubject<boolean>;
|
|
||||||
} = {
|
|
||||||
categorySlug$ : new BehaviorSubject('all'),
|
|
||||||
query$ : new BehaviorSubject(''),
|
|
||||||
hideCompleted$: new BehaviorSubject(false)
|
|
||||||
};
|
|
||||||
|
|
||||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
private _activatedRoute: ActivatedRoute,
|
|
||||||
private _changeDetectorRef: ChangeDetectorRef,
|
|
||||||
private _router: Router,
|
|
||||||
private _academyService: AcademyService
|
|
||||||
)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ Lifecycle hooks
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On init
|
|
||||||
*/
|
|
||||||
ngOnInit(): void
|
|
||||||
{
|
|
||||||
// Get the categories
|
|
||||||
this._academyService.categories$
|
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
|
||||||
.subscribe((categories: Category[]) => {
|
|
||||||
this.categories = categories;
|
|
||||||
|
|
||||||
// Mark for check
|
|
||||||
this._changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the courses
|
|
||||||
this._academyService.courses$
|
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
|
||||||
.subscribe((courses: Course[]) => {
|
|
||||||
this.courses = this.filteredCourses = courses;
|
|
||||||
|
|
||||||
// Mark for check
|
|
||||||
this._changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter the courses
|
|
||||||
combineLatest([this.filters.categorySlug$, this.filters.query$, this.filters.hideCompleted$])
|
|
||||||
.subscribe(([categorySlug, query, hideCompleted]) => {
|
|
||||||
|
|
||||||
// Reset the filtered courses
|
|
||||||
this.filteredCourses = this.courses;
|
|
||||||
|
|
||||||
// Filter by category
|
|
||||||
if ( categorySlug !== 'all' )
|
|
||||||
{
|
|
||||||
this.filteredCourses = this.filteredCourses.filter((course) => course.category === categorySlug);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by search query
|
|
||||||
if ( query !== '' )
|
|
||||||
{
|
|
||||||
this.filteredCourses = this.filteredCourses.filter((course) => {
|
|
||||||
return course.title.toLowerCase().includes(query.toLowerCase())
|
|
||||||
|| course.description.toLowerCase().includes(query.toLowerCase())
|
|
||||||
|| course.category.toLowerCase().includes(query.toLowerCase());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by completed
|
|
||||||
if ( hideCompleted )
|
|
||||||
{
|
|
||||||
this.filteredCourses = this.filteredCourses.filter((course) => course.progress.completed === 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On destroy
|
|
||||||
*/
|
|
||||||
ngOnDestroy(): void
|
|
||||||
{
|
|
||||||
// Unsubscribe from all subscriptions
|
|
||||||
this._unsubscribeAll.next();
|
|
||||||
this._unsubscribeAll.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ Public methods
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter by search query
|
|
||||||
*
|
|
||||||
* @param query
|
|
||||||
*/
|
|
||||||
filterByQuery(query: string): void
|
|
||||||
{
|
|
||||||
this.filters.query$.next(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter by category
|
|
||||||
*
|
|
||||||
* @param change
|
|
||||||
*/
|
|
||||||
filterByCategory(change: MatSelectChange): void
|
|
||||||
{
|
|
||||||
this.filters.categorySlug$.next(change.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show/hide completed courses
|
|
||||||
*
|
|
||||||
* @param change
|
|
||||||
*/
|
|
||||||
toggleCompleted(change: MatSlideToggleChange): void
|
|
||||||
{
|
|
||||||
this.filters.hideCompleted$.next(change.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Track by function for ngFor loops
|
|
||||||
*
|
|
||||||
* @param index
|
|
||||||
* @param item
|
|
||||||
*/
|
|
||||||
trackByFn(index: number, item: any): any
|
|
||||||
{
|
|
||||||
return item.id || index;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user