mirror of
https://github.com/richard-loafle/fuse-angular.git
synced 2025-01-08 03:25:08 +00:00
(apps/academy) New version of the Academy app
This commit is contained in:
parent
63edc8d1f2
commit
bb0efade72
|
@ -56,7 +56,7 @@ const utilities = plugin(({
|
|||
}
|
||||
},
|
||||
{
|
||||
variants: ['dark', 'responsive']
|
||||
variants: ['dark', 'responsive', 'group-hover', 'hover']
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -82,6 +82,7 @@ export const appRoutes: Route[] = [
|
|||
|
||||
// Apps
|
||||
{path: 'apps', children: [
|
||||
{path: 'academy', loadChildren: () => import('app/modules/admin/apps/academy/academy.module').then(m => m.AcademyModule)},
|
||||
{path: 'calendar', loadChildren: () => import('app/modules/admin/apps/calendar/calendar.module').then(m => m.CalendarModule)},
|
||||
{path: 'contacts', loadChildren: () => import('app/modules/admin/apps/contacts/contacts.module').then(m => m.ContactsModule)},
|
||||
{path: 'ecommerce', loadChildren: () => import('app/modules/admin/apps/ecommerce/ecommerce.module').then(m => m.ECommerceModule)},
|
||||
|
|
86
src/app/mock-api/apps/academy/api.ts
Normal file
86
src/app/mock-api/apps/academy/api.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { FuseMockApiService } from '@fuse/lib/mock-api/mock-api.service';
|
||||
import { categories as categoriesData, courses as coursesData, demoCourseSteps as demoCourseStepsData } from 'app/mock-api/apps/academy/data';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AcademyMockApi
|
||||
{
|
||||
private _categories: any[] = categoriesData;
|
||||
private _courses: any[] = coursesData;
|
||||
private _demoCourseSteps: any[] = demoCourseStepsData;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _fuseMockApiService: FuseMockApiService)
|
||||
{
|
||||
// Register Mock API handlers
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Register Mock API handlers
|
||||
*/
|
||||
registerHandlers(): void
|
||||
{
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Categories - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/apps/academy/categories')
|
||||
.reply(() => {
|
||||
|
||||
// Clone the categories
|
||||
const categories = cloneDeep(this._categories);
|
||||
|
||||
// Sort the categories alphabetically by title
|
||||
categories.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return [200, categories];
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Courses - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/apps/academy/courses')
|
||||
.reply(() => {
|
||||
|
||||
// Clone the courses
|
||||
const courses = cloneDeep(this._courses);
|
||||
|
||||
return [200, courses];
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Course - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/apps/academy/courses/course')
|
||||
.reply(({request}) => {
|
||||
|
||||
// Get the id from the params
|
||||
const id = request.params.get('id');
|
||||
|
||||
// Clone the courses and steps
|
||||
const courses = cloneDeep(this._courses);
|
||||
const steps = cloneDeep(this._demoCourseSteps);
|
||||
|
||||
// Find the course and attach steps to it
|
||||
const course = courses.find((item) => item.id === id);
|
||||
course.steps = steps;
|
||||
|
||||
return [
|
||||
200,
|
||||
course
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
719
src/app/mock-api/apps/academy/data.ts
Normal file
719
src/app/mock-api/apps/academy/data.ts
Normal file
|
@ -0,0 +1,719 @@
|
|||
/* tslint:disable:max-line-length */
|
||||
export const categories = [
|
||||
{
|
||||
id : '9a67dff7-3c38-4052-a335-0cef93438ff6',
|
||||
title: 'Web',
|
||||
slug : 'web'
|
||||
},
|
||||
{
|
||||
id : 'a89672f5-e00d-4be4-9194-cb9d29f82165',
|
||||
title: 'Firebase',
|
||||
slug : 'firebase'
|
||||
},
|
||||
{
|
||||
id : '02f42092-bb23-4552-9ddb-cfdcc235d48f',
|
||||
title: 'Cloud',
|
||||
slug : 'cloud'
|
||||
},
|
||||
{
|
||||
id : '5648a630-979f-4403-8c41-fc9790dea8cd',
|
||||
title: 'Android',
|
||||
slug : 'android'
|
||||
}
|
||||
];
|
||||
export const courses = [
|
||||
{
|
||||
id : '694e4e5f-f25f-470b-bd0e-26b1d4f64028',
|
||||
title : 'Basics of Angular',
|
||||
slug : 'basics-of-angular',
|
||||
description: 'Introductory course for Angular and framework basics',
|
||||
category : 'web',
|
||||
duration : 30,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jun 28, 2021',
|
||||
featured : true,
|
||||
progress : {
|
||||
currentStep: 3,
|
||||
completed : 2
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'f924007a-2ee9-470b-a316-8d21ed78277f',
|
||||
title : 'Basics of TypeScript',
|
||||
slug : 'basics-of-typeScript',
|
||||
description: 'Beginner course for Typescript and its basics',
|
||||
category : 'web',
|
||||
duration : 60,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Nov 01, 2021',
|
||||
featured : true,
|
||||
progress : {
|
||||
currentStep: 5,
|
||||
completed : 3
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '0c06e980-abb5-4ba7-ab65-99a228cab36b',
|
||||
title : 'Android N: Quick Settings',
|
||||
slug : 'android-n-quick-settings',
|
||||
description: 'Step by step guide for Android N: Quick Settings',
|
||||
category : 'android',
|
||||
duration : 120,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'May 08, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 10,
|
||||
completed : 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '1b9a9acc-9a36-403e-a1e7-b11780179e38',
|
||||
title : 'Build an App for the Google Assistant with Firebase',
|
||||
slug : 'build-an-app-for-the-google-assistant-with-firebase',
|
||||
description: 'Dive deep into Google Assistant apps using Firebase',
|
||||
category : 'firebase',
|
||||
duration : 30,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jan 09, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 4,
|
||||
completed : 3
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '55eb415f-3f4e-4853-a22b-f0ae91331169',
|
||||
title : 'Keep Sensitive Data Safe and Private',
|
||||
slug : 'keep-sensitive-data-safe-and-private',
|
||||
description: 'Learn how to keep your important data safe and private',
|
||||
category : 'android',
|
||||
duration : 45,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jan 14, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 6,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'fad2ab23-1011-4028-9a54-e52179ac4a50',
|
||||
title : 'Manage Your Pivotal Cloud Foundry App\'s Using Apigee Edge',
|
||||
slug : 'manage-your-pivotal-cloud-foundry-apps-using-apigee-Edge',
|
||||
description: 'Introductory course for Pivotal Cloud Foundry App',
|
||||
category : 'cloud',
|
||||
duration : 90,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jun 24, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 6,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'c4bc107b-edc4-47a7-a7a8-4fb09732e794',
|
||||
title : 'Build a PWA Using Workbox',
|
||||
slug : 'build-a-pwa-using-workbox',
|
||||
description: 'Step by step guide for building a PWA using Workbox',
|
||||
category : 'web',
|
||||
duration : 120,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Nov 19, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '1449f945-d032-460d-98e3-406565a22293',
|
||||
title : 'Cloud Functions for Firebase',
|
||||
slug : 'cloud-functions-for-firebase',
|
||||
description: 'Beginners guide of Firebase Cloud Functions',
|
||||
category : 'firebase',
|
||||
duration : 45,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jul 11, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 3,
|
||||
completed : 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'f05e08ab-f3e3-4597-a032-6a4b69816f24',
|
||||
title : 'Building a gRPC Service with Java',
|
||||
slug : 'building-a-grpc-service-with-java',
|
||||
description: 'Learn more about building a gRPC Service with Java',
|
||||
category : 'cloud',
|
||||
duration : 30,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Mar 13, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '181728f4-87c8-45c5-b9cc-92265bcd2f4d',
|
||||
title : 'Looking at Campaign Finance with BigQuery',
|
||||
slug : 'looking-at-campaign-finance-with-bigquery',
|
||||
description: 'Dive deep into BigQuery: Campaign Finance',
|
||||
category : 'cloud',
|
||||
duration : 60,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Nov 01, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'fcbfedbf-6187-4b3b-89d3-1a7cb4e11616',
|
||||
title : 'Personalize Your iOS App with Firebase User Management',
|
||||
slug : 'personalize-your-ios-app-with-firebase-user-management',
|
||||
description: 'Dive deep into User Management on iOS apps using Firebase',
|
||||
category : 'firebase',
|
||||
duration : 90,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Aug 08, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '5213f6a1-1dd7-4b1d-b6e9-ffb7af534f28',
|
||||
title : 'Customize Network Topology with Subnetworks',
|
||||
slug : 'customize-network-topology-with-subnetworks',
|
||||
description: 'Dive deep into Network Topology with Subnetworks',
|
||||
category : 'web',
|
||||
duration : 45,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'May 12, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '02992ac9-d1a3-4167-b70e-8a1d5b5ba253',
|
||||
title : 'Building Beautiful UIs with Flutter',
|
||||
slug : 'building-beautiful-uis-with-flutter',
|
||||
description: 'Dive deep into Flutter\'s hidden secrets for creating beautiful UIs',
|
||||
category : 'web',
|
||||
duration : 90,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Sep 18, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 8,
|
||||
completed : 2
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '2139512f-41fb-4a4a-841a-0b4ac034f9b4',
|
||||
title : 'Firebase Android',
|
||||
slug : 'firebase-android',
|
||||
description: 'Beginners guide of Firebase for Android',
|
||||
category : 'android',
|
||||
duration : 45,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Apr 24, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '65e0a0e0-d8c0-4117-a3cb-eb74f8e28809',
|
||||
title : 'Simulating a Thread Network Using OpenThread',
|
||||
slug : 'simulating-a-thread-network-using-openthread',
|
||||
description: 'Introductory course for OpenThread and Simulating a Thread Network',
|
||||
category : 'web',
|
||||
duration : 45,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jun 05, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'c202ebc9-9be3-433a-9d38-7003b3ed7b7a',
|
||||
title : 'Your First Progressive Web App',
|
||||
slug : 'your-first-progressive-web-app',
|
||||
description: 'Step by step guide for creating a PWA from scratch',
|
||||
category : 'web',
|
||||
duration : 30,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Oct 14, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '980ae7da-9f77-4e30-aa98-1b1ea594e775',
|
||||
title : 'Launch Cloud Datalab',
|
||||
slug : 'launch-cloud-datalab',
|
||||
description: 'From start to finish: Launch Cloud Datalab',
|
||||
category : 'cloud',
|
||||
duration : 60,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Dec 16, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'c9748ea9-4117-492c-bdb2-55085b515978',
|
||||
title : 'Cloud Firestore',
|
||||
slug : 'cloud-firestore',
|
||||
description: 'Step by step guide for setting up Cloud Firestore',
|
||||
category : 'firebase',
|
||||
duration : 90,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Apr 04, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 2,
|
||||
completed : 0
|
||||
}
|
||||
}
|
||||
];
|
||||
export const demoCourseContent = `
|
||||
<p class="lead">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus aperiam lab et fugiat id magnam minus nemo quam
|
||||
voluptatem. Culpa deleniti explica nisi quod soluta.
|
||||
</p>
|
||||
<p>
|
||||
Alias animi labque, deserunt distinctio eum excepturi fuga iure labore magni molestias mollitia natus, officia pofro
|
||||
quis sunt temporibus veritatis voluptatem, voluptatum. Aut blanditiis esse et illum maxim, obcaecati possimus
|
||||
voluptate! Accusamus <em>adipisci</em> amet aperiam, assumenda consequuntur fugiat inventore iusto magnam molestias
|
||||
natus necessitatibus, nulla pariatur.
|
||||
</p>
|
||||
<p>
|
||||
Amet distinctio enim itaque minima minus nesciunt recusandae soluta voluptatibus:
|
||||
</p>
|
||||
<blockquote>
|
||||
<p>
|
||||
Ad aliquid amet asperiores lab distinctio doloremque <code>eaque</code>, exercitationem explicabo, minus mollitia
|
||||
natus necessitatibus odio omnis pofro rem.
|
||||
</p>
|
||||
</blockquote>
|
||||
<p>
|
||||
Alias architecto asperiores, dignissimos illum ipsam ipsum itaque, natus necessitatibus officiis, perferendis quae
|
||||
sed ullam veniam vitae voluptas! Magni, nisi, quis! A <code>accusamus</code> animi commodi, consectetur distinctio
|
||||
eaque, eos excepturi illum laboriosam maiores nam natus nulla officiis perspiciatis rem <em>reprehenderit</em> sed
|
||||
tenetur veritatis.
|
||||
</p>
|
||||
<p>
|
||||
Consectetur <code>dicta enim</code> error eveniet expedita, facere in itaque labore <em>natus</em> quasi? Ad consectetur
|
||||
eligendi facilis magni quae quis, quo temporibus voluptas voluptate voluptatem!
|
||||
</p>
|
||||
<p>
|
||||
Adipisci alias animi <code>debitis</code> eos et impedit maiores, modi nam nobis officia optio perspiciatis, rerum.
|
||||
Accusantium esse nostrum odit quis quo:
|
||||
</p>
|
||||
<pre><code>h1 a {{'{'}}
|
||||
display: block;
|
||||
width: 300px;
|
||||
height: 80px;
|
||||
{{'}'}}</code></pre>
|
||||
<p>
|
||||
Accusantium aut autem, lab deleniti eaque fugiat fugit id ipsa iste molestiae,
|
||||
<a>necessitatibus nemo quasi</a>
|
||||
.
|
||||
</p>
|
||||
<hr>
|
||||
<h2>
|
||||
Accusantium aspernatur autem enim
|
||||
</h2>
|
||||
<p>
|
||||
Blanditiis, fugit voluptate! Assumenda blanditiis consectetur, labque cupiditate ducimus eaque earum, fugiat illum
|
||||
ipsa, necessitatibus omnis quaerat reiciendis totam. Architecto, <strong>facere</strong> illum molestiae nihil nulla
|
||||
quibusdam quidem vel! Atque <em>blanditiis deserunt</em>.
|
||||
</p>
|
||||
<p>
|
||||
Debitis deserunt doloremque labore laboriosam magni minus odit:
|
||||
</p>
|
||||
<ol>
|
||||
<li>Asperiores dicta esse maiores nobis officiis.</li>
|
||||
<li>Accusamus aliquid debitis dolore illo ipsam molettiae possimus.</li>
|
||||
<li>Magnam mollitia pariatur perspiciatis quasi quidem tenetur voluptatem! Adipisci aspernatur assumenda dicta.</li>
|
||||
</ol>
|
||||
<p>
|
||||
Animi fugit incidunt iure magni maiores molestias.
|
||||
</p>
|
||||
<h3>
|
||||
Consequatur iusto soluta
|
||||
</h3>
|
||||
<p>
|
||||
Aliquid asperiores corporis — deserunt dolorum ducimus eius eligendi explicabo quaerat suscipit voluptas.
|
||||
</p>
|
||||
<p>
|
||||
Deserunt dolor eos et illum laborum magni molestiae mollitia:
|
||||
</p>
|
||||
<blockquote>
|
||||
<p>Autem beatae consectetur consequatur, facere, facilis fugiat id illo, impedit numquam optio quis sunt ducimus illo.</p>
|
||||
</blockquote>
|
||||
<p>
|
||||
Adipisci consequuntur doloribus facere in ipsam maxime molestias pofro quam:
|
||||
</p>
|
||||
<figure>
|
||||
<img
|
||||
src="assets/images/pages/help-center/image-1.jpg"
|
||||
alt="">
|
||||
<figcaption>
|
||||
Accusamus blanditiis labque delectus esse et eum excepturi, impedit ipsam iste maiores minima mollitia, nihil obcaecati
|
||||
placeat quaerat qui quidem sint unde!
|
||||
</figcaption>
|
||||
</figure>
|
||||
<p>
|
||||
A beatae lab deleniti explicabo id inventore magni nisi omnis placeat praesentium quibusdam:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Dolorem eaque laboriosam omnis praesentium.</li>
|
||||
<li>Atque debitis delectus distinctio doloremque.</li>
|
||||
<li>Fuga illo impedit minima mollitia neque obcaecati.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Consequ eius eum excepturi explicabo.
|
||||
</p>
|
||||
<h2>
|
||||
Adipisicing elit atque impedit?
|
||||
</h2>
|
||||
<h3>
|
||||
Atque distinctio doloremque ea qui quo, repellendus.
|
||||
</h3>
|
||||
<p>
|
||||
Delectus deserunt explicabo facilis numquam quasi! Laboriosam, magni, quisquam. Aut, blanditiis commodi distinctio, facere fuga
|
||||
hic itaque iure labore laborum maxime nemo neque provident quos recusandae sequi veritatis illum inventore iure qui rerum sapiente.
|
||||
</p>
|
||||
<h3>
|
||||
Accusamus iusto sint aperiam consectetur …
|
||||
</h3>
|
||||
<p>
|
||||
Aliquid assumenda ipsa nam odit pofro quaerat, quasi recusandae sint! Aut, esse explicabo facilis fugit illum iure magni
|
||||
necessitatibus odio quas.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p><strong>Dolore natus placeat rem atque dignissimos laboriosam.</strong></p>
|
||||
<p>
|
||||
Amet repudiandae voluptates architecto dignissimos repellendus voluptas dignissimos eveniet itaque maiores natus.
|
||||
</p>
|
||||
<p>
|
||||
Accusamus aliquam debitis delectus dolorem ducimus enim eos, exercitationem fugiat id iusto nostrum quae quos
|
||||
recusandae reiciendis rerum sequi temporibus veniam vero? Accusantium culpa, cupiditate ducimus eveniet id maiores modi
|
||||
mollitia nisi aliquid dolorum ducimus et illo in.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Ab amet deleniti dolor, et hic optio placeat.</strong></p>
|
||||
<p>
|
||||
Accusantium ad alias beatae, consequatur consequuntur eos error eveniet expedita fuga laborum libero maxime nulla pofro
|
||||
praesentium rem rerum saepe soluta ullam vero, voluptas? Architecto at debitis, doloribus harum iure libero natus odio
|
||||
optio soluta veritatis voluptate.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>At aut consectetur nam necessitatibus neque nesciunt.</strong></p>
|
||||
<p>
|
||||
Aut dignissimos labore nobis nostrum optio! Dolor id minima velit voluptatibus. Aut consequuntur eum exercitationem
|
||||
fuga, harum id impedit molestiae natus neque numquam perspiciatis quam rem voluptatum.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Animi aperiam autem labque dolore enim ex expedita harum hic id impedit ipsa laborum modi mollitia non perspiciatis quae ratione.
|
||||
</p>
|
||||
<h2>
|
||||
Alias eos excepturi facilis fugit.
|
||||
</h2>
|
||||
<p>
|
||||
Alias asperiores, aspernatur corporis
|
||||
<a>delectus</a>
|
||||
est
|
||||
<a>facilis</a>
|
||||
inventore dolore
|
||||
ipsa nobis nostrum officia quia, veritatis vero! At dolore est nesciunt numquam quam. Ab animi <em>architecto</em> aut, dignissimos
|
||||
eos est eum explicabo.
|
||||
</p>
|
||||
<p>
|
||||
Adipisci autem consequuntur <code>labque cupiditate</code> dolor ducimus fuga neque nesciunt:
|
||||
</p>
|
||||
<pre><code>module.exports = {{'{'}}
|
||||
purge: [],
|
||||
theme: {{'{'}}
|
||||
extend: {{'{}'}},
|
||||
},
|
||||
variants: {{'{}'}},
|
||||
plugins: [],
|
||||
{{'}'}}</code></pre>
|
||||
<p>
|
||||
Aliquid aspernatur eius fugit hic iusto.
|
||||
</p>
|
||||
<h3>
|
||||
Dolorum ducimus expedita?
|
||||
</h3>
|
||||
<p>
|
||||
Culpa debitis explicabo maxime minus quaerat reprehenderit temporibus! Asperiores, cupiditate ducimus esse est expedita fuga hic
|
||||
ipsam necessitatibus placeat possimus? Amet animi aut consequuntur earum eveniet.
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>Aspernatur at beatae corporis debitis.</strong>
|
||||
<ul>
|
||||
<li>
|
||||
Aperiam assumenda commodi lab dicta eius, “fugit ipsam“ itaque iure molestiae nihil numquam, officia omnis quia
|
||||
repellendus sapiente sed.
|
||||
</li>
|
||||
<li>
|
||||
Nulla odio quod saepe accusantium, adipisci autem blanditiis lab doloribus.
|
||||
</li>
|
||||
<li>
|
||||
Explicabo facilis iusto molestiae nisi nostrum obcaecati officia.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Nobis odio officiis optio quae quis quisquam quos rem.</strong>
|
||||
<ul>
|
||||
<li>Modi pariatur quod totam. Deserunt doloribus eveniet, expedita.</li>
|
||||
<li>Ad beatae dicta et fugit libero optio quaerat rem repellendus./</li>
|
||||
<li>Architecto atque consequuntur corporis id iste magni.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Deserunt non placeat unde veniam veritatis? Odio quod.</strong>
|
||||
<ul>
|
||||
<li>Inventore iure magni quod repellendus tempora. Magnam neque, quia. Adipisci amet.</li>
|
||||
<li>Consectetur adipisicing elit.</li>
|
||||
<li>labque eum expedita illo inventore iusto laboriosam nesciunt non, odio provident.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
A aliquam architecto consequatur labque dicta doloremque <code><li></code> doloribus, ducimus earum, est <code><p></code>
|
||||
eveniet explicabo fuga fugit ipsum minima minus molestias nihil nisi non qui sunt vel voluptatibus? A dolorum illum nihil quidem.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Laboriosam nesciunt obcaecati optio qui.</strong>
|
||||
</p>
|
||||
<p>
|
||||
Doloremque magni molestias reprehenderit.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Accusamus aperiam blanditiis <code><p></code> commodi</li>
|
||||
<li>Dolorum ea explicabo fugiat in ipsum</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Commodi dolor dolorem dolores eum expedita libero.</strong>
|
||||
</p>
|
||||
<p>
|
||||
Accusamus alias consectetur dolores et, excepturi fuga iusto possimus.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
Accusantium ad alias atque aut autem consequuntur deserunt dignissimos eaque iure <code><p></code> maxime.
|
||||
</p>
|
||||
<p>
|
||||
Dolorum in nisi numquam omnis quam sapiente sit vero.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Adipisci lab in nisi odit soluta sunt vitae commodi excepturi.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Assumenda deserunt distinctio dolor iste mollitia nihil non?
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Consectetur adipisicing elit dicta dolor iste.
|
||||
</p>
|
||||
<h2>
|
||||
Consectetur ea natus officia omnis reprehenderit.
|
||||
</h2>
|
||||
<p>
|
||||
Distinctio impedit quaerat sed! Accusamus
|
||||
<a>aliquam aspernatur enim expedita explicabo</a>
|
||||
. Libero molestiae
|
||||
odio quasi unde ut? Ab exercitationem id numquam odio quisquam!
|
||||
</p>
|
||||
<p>
|
||||
Explicabo facilis nemo quidem natus tempore:
|
||||
</p>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Wrestler</th>
|
||||
<th>Origin</th>
|
||||
<th>Finisher</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Bret “The Hitman” Hart</td>
|
||||
<td>Calgary, AB</td>
|
||||
<td>Sharpshooter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stone Cold Steve Austin</td>
|
||||
<td>Austin, TX</td>
|
||||
<td>Stone Cold Stunner</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Randy Savage</td>
|
||||
<td>Sarasota, FL</td>
|
||||
<td>Elbow Drop</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vader</td>
|
||||
<td>Boulder, CO</td>
|
||||
<td>Vader Bomb</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Razor Ramon</td>
|
||||
<td>Chuluota, FL</td>
|
||||
<td>Razor’s Edge</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
A aliquid autem lab doloremque, ea earum eum fuga fugit illo ipsa minus natus nisi <code><span></code> obcaecati pariatur
|
||||
perferendis pofro <code>suscipit tempore</code>.
|
||||
</p>
|
||||
<h3>
|
||||
Ad alias atque culpa <code>illum</code> earum optio
|
||||
</h3>
|
||||
<p>
|
||||
Architecto consequatur eveniet illo in iure laborum minus omnis quibusdam sequi temporibus? Ab aliquid <em>“atque dolores molestiae
|
||||
nemo perferendis”</em> reprehenderit saepe.
|
||||
</p>
|
||||
<p>
|
||||
Accusantium aliquid eligendi est fuga natus, <code>quos</code> vel? Adipisci aperiam asperiores aspernatur consectetur cupiditate
|
||||
<a><code>@distinctio/doloribus</code></a>
|
||||
et exercitationem expedita, facere facilis illum, impedit inventore
|
||||
ipsa iure iusto magnam, magni minus nesciunt non officia possimus quod reiciendis.
|
||||
</p>
|
||||
<h4>
|
||||
Cupiditate explicabo <code>hic</code> maiores
|
||||
</h4>
|
||||
<p>
|
||||
Aliquam amet consequuntur distinctio <code>ea</code> est <code>excepturi</code> facere illum maiores nisi nobis non odit officiis
|
||||
quisquam, similique tempora temporibus, tenetur ullam <code>voluptates</code> adipisci aperiam deleniti <code>doloremque</code>
|
||||
ducimus <code>eos</code>.
|
||||
</p>
|
||||
<p>
|
||||
Ducimus qui quo tempora. lab enim explicabo <code>hic</code> inventore qui soluta voluptates voluptatum? Asperiores consectetur
|
||||
delectus dolorem fugiat ipsa pariatur, quas <code>quos</code> repellendus <em>repudiandae</em> sunt aut blanditiis.
|
||||
</p>
|
||||
<h3>
|
||||
Asperiores aspernatur autem error praesentium quidem.
|
||||
</h3>
|
||||
<h4>
|
||||
Ad blanditiis commodi, doloribus id iste <code>repudiandae</code> vero vitae.
|
||||
</h4>
|
||||
<p>
|
||||
Atque consectetur lab debitis enim est et, facere fugit impedit, possimus quaerat quibusdam.
|
||||
</p>
|
||||
<p>
|
||||
Dolorem nihil placeat quibusdam veniam? Amet architecto at consequatur eligendi eveniet excepturi hic illo impedit in iste magni maxime
|
||||
modi nisi nulla odio placeat quidem, quos rem repellat similique suscipit voluptate voluptates nobis omnis quo repellendus.
|
||||
</p>
|
||||
<p>
|
||||
Assumenda, eum, minima! Autem consectetur fugiat iste sit! Nobis omnis quo repellendus.
|
||||
</p>
|
||||
`;
|
||||
export const demoCourseSteps = [
|
||||
{
|
||||
order : 0,
|
||||
title : 'Introduction',
|
||||
subtitle: 'Introducing the library and how it works',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Introduction</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 1,
|
||||
title : 'Get the sample code',
|
||||
subtitle: 'Where to find the sample code and how to access it',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Get the sample code</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 2,
|
||||
title : 'Create a Firebase project and Set up your app',
|
||||
subtitle: 'How to create a basic Firebase project and how to run it locally',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Create a Firebase project and Set up your app</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 3,
|
||||
title : 'Install the Firebase Command Line Interface',
|
||||
subtitle: 'Setting up the Firebase CLI to access command line tools',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Install the Firebase Command Line Interface</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 4,
|
||||
title : 'Deploy and run the web app',
|
||||
subtitle: 'How to build, push and run the project remotely',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Deploy and run the web app</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 5,
|
||||
title : 'The Functions Directory',
|
||||
subtitle: 'Introducing the Functions and Functions Directory',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">The Functions Directory</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 6,
|
||||
title : 'Import the Cloud Functions and Firebase Admin modules',
|
||||
subtitle: 'Create your first Function and run it to administer your app',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Import the Cloud Functions and Firebase Admin modules</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 7,
|
||||
title : 'Welcome New Users',
|
||||
subtitle: 'How to create a welcome message for the new users',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Welcome New Users</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 8,
|
||||
title : 'Images moderation',
|
||||
subtitle: 'How to moderate images; crop, resize, optimize',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Images moderation</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 9,
|
||||
title : 'New Message Notifications',
|
||||
subtitle: 'How to create and push a notification to a user',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">New Message Notifications</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 10,
|
||||
title : 'Congratulations!',
|
||||
subtitle: 'Nice work, you have created your first application',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Congratulations!</h1> ${demoCourseContent}`
|
||||
}
|
||||
];
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
/* tslint:disable:max-line-length */
|
||||
import { FuseNavigationItem } from '@fuse/components/navigation';
|
||||
|
||||
|
@ -33,6 +32,13 @@ export const defaultNavigation: FuseNavigationItem[] = [
|
|||
type : 'group',
|
||||
icon : 'heroicons_outline:home',
|
||||
children: [
|
||||
{
|
||||
id : 'apps.academy',
|
||||
title: 'Academy',
|
||||
type : 'basic',
|
||||
icon : 'heroicons_outline:academic-cap',
|
||||
link : '/apps/academy'
|
||||
},
|
||||
{
|
||||
id : 'apps.calendar',
|
||||
title : 'Calendar',
|
||||
|
@ -1135,6 +1141,13 @@ export const futuristicNavigation: FuseNavigationItem[] = [
|
|||
icon : 'heroicons_outline:clipboard-check',
|
||||
link : '/dashboards/project'
|
||||
},
|
||||
{
|
||||
id : 'apps.academy',
|
||||
title: 'Academy',
|
||||
type : 'basic',
|
||||
icon : 'heroicons_outline:academic-cap',
|
||||
link : '/apps/academy'
|
||||
},
|
||||
{
|
||||
id : 'apps.calendar',
|
||||
title: 'Calendar',
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { AcademyMockApi } from 'app/mock-api/apps/academy/api';
|
||||
import { AnalyticsMockApi } from 'app/mock-api/dashboards/analytics/api';
|
||||
import { AuthMockApi } from 'app/mock-api/common/auth/api';
|
||||
import { CalendarMockApi } from 'app/mock-api/apps/calendar/api';
|
||||
|
@ -17,6 +18,7 @@ import { TasksMockApi } from 'app/mock-api/apps/tasks/api';
|
|||
import { UserMockApi } from 'app/mock-api/common/user/api';
|
||||
|
||||
export const mockApiServices = [
|
||||
AcademyMockApi,
|
||||
AnalyticsMockApi,
|
||||
AuthMockApi,
|
||||
CalendarMockApi,
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<router-outlet></router-outlet>
|
17
src/app/modules/admin/apps/academy/academy.component.ts
Normal file
17
src/app/modules/admin/apps/academy/academy.component.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
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()
|
||||
{
|
||||
}
|
||||
}
|
44
src/app/modules/admin/apps/academy/academy.module.ts
Normal file
44
src/app/modules/admin/apps/academy/academy.module.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
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
|
||||
{
|
||||
}
|
110
src/app/modules/admin/apps/academy/academy.resolvers.ts
Normal file
110
src/app/modules/admin/apps/academy/academy.resolvers.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
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);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
32
src/app/modules/admin/apps/academy/academy.routing.ts
Normal file
32
src/app/modules/admin/apps/academy/academy.routing.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
91
src/app/modules/admin/apps/academy/academy.service.ts
Normal file
91
src/app/modules/admin/apps/academy/academy.service.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
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);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
29
src/app/modules/admin/apps/academy/academy.types.ts
Normal file
29
src/app/modules/admin/apps/academy/academy.types.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
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;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
<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>
|
||||
<button
|
||||
class="flex-0 ml-0.5"
|
||||
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>
|
204
src/app/modules/admin/apps/academy/details/details.component.ts
Normal file
204
src/app/modules/admin/apps/academy/details/details.component.ts
Normal file
|
@ -0,0 +1,204 @@
|
|||
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'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
196
src/app/modules/admin/apps/academy/list/list.component.html
Normal file
196
src/app/modules/admin/apps/academy/list/list.component.html
Normal file
|
@ -0,0 +1,196 @@
|
|||
<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>
|
159
src/app/modules/admin/apps/academy/list/list.component.ts
Normal file
159
src/app/modules/admin/apps/academy/list/list.component.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -412,7 +412,7 @@ const config = {
|
|||
pointerEvents : ['responsive'],
|
||||
position : ['responsive'],
|
||||
resize : [],
|
||||
ringColor : ['dark'],
|
||||
ringColor : ['dark', 'group-hover'],
|
||||
ringOffsetColor : ['dark'],
|
||||
ringOffsetWidth : [],
|
||||
ringOpacity : [],
|
||||
|
|
Loading…
Reference in New Issue
Block a user