(apps/academy) New version of the Academy app

This commit is contained in:
sercan 2021-04-25 23:23:23 +03:00
parent 63edc8d1f2
commit bb0efade72
18 changed files with 1902 additions and 3 deletions

View File

@ -56,7 +56,7 @@ const utilities = plugin(({
}
},
{
variants: ['dark', 'responsive']
variants: ['dark', 'responsive', 'group-hover', 'hover']
}
);

View File

@ -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)},

View 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
];
});
}
}

View 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>&lt;li&gt;</code> doloribus, ducimus earum, est <code>&lt;p&gt;</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>&lt;p&gt;</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>&lt;p&gt;</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>Razors Edge</td>
</tr>
</tbody>
</table>
<p>
A aliquid autem lab doloremque, ea earum eum fuga fugit illo ipsa minus natus nisi <code>&lt;span&gt;</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}`
}
];

View File

@ -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',

View File

@ -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,

View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

View 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()
{
}
}

View 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
{
}

View 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);
})
);
}
}

View 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
}
}
]
}
];

View 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);
})
);
}
}

View 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;
};
}

View File

@ -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>

View 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'
});
}
});
}
}

View 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>

View 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;
}
}

View File

@ -412,7 +412,7 @@ const config = {
pointerEvents : ['responsive'],
position : ['responsive'],
resize : [],
ringColor : ['dark'],
ringColor : ['dark', 'group-hover'],
ringOffsetColor : ['dark'],
ringOffsetWidth : [],
ringOpacity : [],