Compare commits

..

33 Commits

Author SHA1 Message Date
sercan
4ccce1b423 Increased the version number
(changelog) Updated the changelog
2021-05-06 17:11:12 +03:00
sercan
f6b4ca0880 (apps/notes) Responsive adjustments 2021-05-06 17:05:56 +03:00
sercan
77014174e8 (apps/notes) New version of the Notes app 2021-05-06 17:01:14 +03:00
sercan
5ac7002a98 (fuse/masonry) Fixed: Masonry doesn't work with the data that comes from async pipe 2021-05-06 12:07:07 +03:00
sercan
b0f1e1de95 (apps/mailbox) App title font size adjustment for better consistency 2021-05-05 17:52:10 +03:00
sercan
cf01383358 (apps/mailbox) Use shadow on threads for better consistency 2021-05-05 17:27:39 +03:00
sercan
e4442d683b (apps/tasks) Tweaked the hover color on task list for better consistency 2021-05-05 17:27:12 +03:00
sercan
623b43a94c (fuse/styles) Fixed: fuse-highlight doesn't have a margin around in Docs 2021-05-03 18:49:00 +03:00
sercan
e7a1d386a6 (fuse/masonry) Added a new component (and its docs) for creating Masonry layouts 2021-05-03 18:47:57 +03:00
sercan
b05763135e (apps/chat) Adjustments and optimizations for smaller devices 2021-04-30 19:55:37 +03:00
sercan
5dd60c816c (apps/chat) Small adjustments and tweaks for Dark mode 2021-04-30 19:39:02 +03:00
sercan
0ac967a945 Removed optional chaining operators to support Node v12
Set the version on .nvmrc to 12
2021-04-30 19:27:40 +03:00
sercan
e3821da077 (apps/chat) New and improved Chat app 2021-04-30 19:18:09 +03:00
sercan
ee48e11548 Increased the version number
(dependencies) Updated Angular, Angular Material and various other packages
(changelog) Updated the changelog
2021-04-30 19:07:53 +03:00
sercan
215546cc31 (apps/academy) Removed a misplaced import 2021-04-29 19:57:58 +03:00
sercan
072dbce6d4 (fuse/fullscreen) Added Fullscreen toggle component 2021-04-28 10:55:32 +03:00
sercan
5ffe0d0efa (pages/pricing) Improved the spacing of the CTA section on all pricing pages 2021-04-26 23:42:16 +03:00
sercan
e90fb9e618 (apps/academy) Added missing trackBy functions to '*ngFor's 2021-04-26 16:41:21 +03:00
sercan
88e98d002d (apps/mailbox) Removed unused methods 2021-04-26 16:08:19 +03:00
sercan
deeef323f9 (apps/academy) Better error handling on courses that are not exist 2021-04-26 15:59:44 +03:00
sercan
284e282761 Remove quotes from .nvmrc version info 2021-04-26 10:23:08 +03:00
sercan
52e234325f Added .nvmrc with required Node version 2021-04-26 10:19:52 +03:00
sercan
42e0864538 (dependencies) Updated Angular, Angular Material and various other packages
(changelog) Added changelog and updated the version number
2021-04-26 09:55:41 +03:00
sercan
6b6442b37f (apps/academy) Added page info in between the desktop navigation buttons 2021-04-26 09:29:59 +03:00
sercan
bb0efade72 (apps/academy) New version of the Academy app 2021-04-25 23:23:23 +03:00
sercan
63edc8d1f2 (layouts/classy) Disabled footer on Classy layout for more 'classy' look 2021-04-25 23:15:38 +03:00
sercan
9dde624bb5 (icons) Added Material Solid icons 2021-04-25 23:14:13 +03:00
sercan
a5a27d0a51 (icons) Added Material Solid icons 2021-04-25 13:23:26 +03:00
sercan
85ea34a6ce (Contacts) Fixed: Clicking on the tag checkbox on tag panel breaks the toggling 2021-04-25 12:42:41 +03:00
sercan
9b059f8d0d (icons/heroicons) Updated heroicons to v1.0.1 2021-04-22 22:05:59 +03:00
sercan
df48ad1c56 (apps/file-manager) Removed unnecessary imports 2021-04-22 11:24:16 +03:00
sercan
6a113a5317 (icons/heroicons-outline) Fixed: academic-cap icon path fills breaks the icon 2021-04-22 11:23:21 +03:00
sercan
4b268e5d1b (apps/file-manager) Better grid 2021-04-20 22:44:12 +03:00
114 changed files with 21801 additions and 16005 deletions

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
12

2797
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@fuse/demo",
"version": "12.0.0",
"version": "12.3.0",
"license": "https://themeforest.net/licenses/standard",
"private": true,
"scripts": {
@@ -12,17 +12,17 @@
"e2e": "ng e2e"
},
"dependencies": {
"@angular/animations": "11.2.10",
"@angular/cdk": "11.2.9",
"@angular/common": "11.2.10",
"@angular/compiler": "11.2.10",
"@angular/core": "11.2.10",
"@angular/forms": "11.2.10",
"@angular/material": "11.2.9",
"@angular/material-moment-adapter": "11.2.9",
"@angular/platform-browser": "11.2.10",
"@angular/platform-browser-dynamic": "11.2.10",
"@angular/router": "11.2.10",
"@angular/animations": "11.2.12",
"@angular/cdk": "11.2.11",
"@angular/common": "11.2.12",
"@angular/compiler": "11.2.12",
"@angular/core": "11.2.12",
"@angular/forms": "11.2.12",
"@angular/material": "11.2.11",
"@angular/material-moment-adapter": "11.2.11",
"@angular/platform-browser": "11.2.12",
"@angular/platform-browser-dynamic": "11.2.12",
"@angular/router": "11.2.12",
"@fullcalendar/angular": "4.4.5-beta",
"@fullcalendar/core": "4.4.2",
"@fullcalendar/daygrid": "4.4.2",
@@ -31,37 +31,37 @@
"@fullcalendar/moment": "4.4.2",
"@fullcalendar/rrule": "4.4.2",
"@fullcalendar/timegrid": "4.4.2",
"apexcharts": "3.26.0",
"apexcharts": "3.26.1",
"crypto-js": "3.3.0",
"highlight.js": "10.7.2",
"lodash-es": "4.17.21",
"moment": "2.29.1",
"ng-apexcharts": "1.5.9",
"ngx-markdown": "11.1.2",
"ngx-quill": "13.2.0",
"ngx-markdown": "11.1.3",
"ngx-quill": "13.3.1",
"perfect-scrollbar": "1.5.0",
"quill": "1.3.7",
"rrule": "2.6.8",
"rxjs": "6.6.7",
"tslib": "2.1.0",
"tslib": "2.2.0",
"web-animations-js": "2.3.2",
"zone.js": "0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "0.1102.9",
"@angular/cli": "11.2.9",
"@angular/compiler-cli": "11.2.10",
"@angular/language-service": "11.2.10",
"@angular-devkit/build-angular": "0.1102.11",
"@angular/cli": "11.2.11",
"@angular/compiler-cli": "11.2.12",
"@angular/language-service": "11.2.12",
"@tailwindcss/aspect-ratio": "0.2.0",
"@tailwindcss/line-clamp": "0.2.0",
"@tailwindcss/typography": "0.4.0",
"@types/chroma-js": "2.1.3",
"@types/crypto-js": "3.1.47",
"@types/highlight.js": "10.1.0",
"@types/jasmine": "3.6.8",
"@types/jasmine": "3.6.9",
"@types/lodash": "4.14.168",
"@types/lodash-es": "4.17.4",
"@types/node": "12.20.6",
"@types/node": "12.20.10",
"autoprefixer": "10.2.5",
"chroma-js": "2.1.1",
"codelyzer": "6.0.1",
@@ -73,9 +73,9 @@
"karma-jasmine": "4.0.1",
"karma-jasmine-html-reporter": "1.5.4",
"lodash": "4.17.21",
"postcss": "8.2.10",
"postcss": "8.2.13",
"protractor": "7.0.0",
"tailwindcss": "2.1.1",
"tailwindcss": "2.1.2",
"ts-node": "8.3.0",
"tslint": "6.1.3",
"typescript": "4.1.5"

View File

@@ -0,0 +1,7 @@
<!-- Button -->
<button
mat-icon-button
[matTooltip]="'Toggle Fullscreen'"
(click)="toggleFullscreen()">
<mat-icon [svgIcon]="'heroicons_outline:arrows-expand'"></mat-icon>
</button>

View File

@@ -0,0 +1,164 @@
import { ChangeDetectionStrategy, Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { FSDocument, FSDocumentElement } from '@fuse/components/fullscreen/fullscreen.types';
@Component({
selector : 'fuse-fullscreen',
templateUrl : './fullscreen.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
exportAs : 'fuseFullscreen'
})
export class FuseFullscreenComponent implements OnInit
{
private _fsDoc: FSDocument;
private _fsDocEl: FSDocumentElement;
private _isFullscreen: boolean = false;
/**
* Constructor
*/
constructor(@Inject(DOCUMENT) private _document: Document)
{
this._fsDoc = _document as FSDocument;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
this._fsDocEl = document.documentElement as FSDocumentElement;
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Toggle the fullscreen mode
*/
toggleFullscreen(): void
{
// Check if the fullscreen is open
this._isFullscreen = this._getBrowserFullscreenElement() !== null;
// Toggle the fullscreen
if ( this._isFullscreen )
{
this._closeFullscreen();
}
else
{
this._openFullscreen();
}
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Get browser's fullscreen element
*
* @private
*/
private _getBrowserFullscreenElement(): Element
{
if ( typeof this._fsDoc.fullscreenElement !== 'undefined' )
{
return this._fsDoc.fullscreenElement;
}
if ( typeof this._fsDoc.mozFullScreenElement !== 'undefined' )
{
return this._fsDoc.mozFullScreenElement;
}
if ( typeof this._fsDoc.msFullscreenElement !== 'undefined' )
{
return this._fsDoc.msFullscreenElement;
}
if ( typeof this._fsDoc.webkitFullscreenElement !== 'undefined' )
{
return this._fsDoc.webkitFullscreenElement;
}
throw new Error('Fullscreen mode is not supported by this browser');
}
/**
* Open the fullscreen
*
* @private
*/
private _openFullscreen(): void
{
if ( this._fsDocEl.requestFullscreen )
{
this._fsDocEl.requestFullscreen();
return;
}
// Firefox
if ( this._fsDocEl.mozRequestFullScreen )
{
this._fsDocEl.mozRequestFullScreen();
return;
}
// Chrome, Safari and Opera
if ( this._fsDocEl.webkitRequestFullscreen )
{
this._fsDocEl.webkitRequestFullscreen();
return;
}
// IE/Edge
if ( this._fsDocEl.msRequestFullscreen )
{
this._fsDocEl.msRequestFullscreen();
return;
}
}
/**
* Close the fullscreen
*
* @private
*/
private _closeFullscreen(): void
{
if ( this._fsDoc.exitFullscreen )
{
this._fsDoc.exitFullscreen();
return;
}
// Firefox
if ( this._fsDoc.mozCancelFullScreen )
{
this._fsDoc.mozCancelFullScreen();
return;
}
// Chrome, Safari and Opera
if ( this._fsDoc.webkitExitFullscreen )
{
this._fsDoc.webkitExitFullscreen();
return;
}
// IE/Edge
else if ( this._fsDoc.msExitFullscreen )
{
this._fsDoc.msExitFullscreen();
return;
}
}
}

View File

@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { FuseFullscreenComponent } from '@fuse/components/fullscreen/fullscreen.component';
@NgModule({
declarations: [
FuseFullscreenComponent
],
imports : [
MatButtonModule,
MatIconModule,
MatTooltipModule
],
exports : [
FuseFullscreenComponent
]
})
export class FuseFullscreenModule
{
}

View File

@@ -0,0 +1,16 @@
export interface FSDocument extends HTMLDocument
{
mozFullScreenElement?: Element;
mozCancelFullScreen?: () => void;
msFullscreenElement?: Element;
msExitFullscreen?: () => void;
webkitFullscreenElement?: Element;
webkitExitFullscreen?: () => void;
}
export interface FSDocumentElement extends HTMLElement
{
mozRequestFullScreen?: () => void;
msRequestFullscreen?: () => void;
webkitRequestFullscreen?: () => void;
}

View File

@@ -0,0 +1 @@
export * from '@fuse/components/fullscreen/public-api';

View File

@@ -0,0 +1,3 @@
export * from '@fuse/components/fullscreen/fullscreen.component';
export * from '@fuse/components/fullscreen/fullscreen.module';
export * from '@fuse/components/fullscreen/fullscreen.types';

View File

@@ -0,0 +1 @@
export * from '@fuse/components/card/public-api';

View File

@@ -0,0 +1,3 @@
<div class="flex">
<ng-container *ngTemplateOutlet="columnsTemplate; context: { $implicit: distributedColumns }"></ng-container>
</div>

View File

@@ -0,0 +1,87 @@
import { AfterViewInit, Component, Input, OnChanges, SimpleChanges, TemplateRef, ViewEncapsulation } from '@angular/core';
import { FuseAnimations } from '@fuse/animations';
import { FuseMediaWatcherService } from '@fuse/services/media-watcher';
@Component({
selector : 'fuse-masonry',
templateUrl : './masonry.component.html',
styleUrls : ['./masonry.component.scss'],
encapsulation: ViewEncapsulation.None,
animations : FuseAnimations,
exportAs : 'fuseMasonry'
})
export class FuseMasonryComponent implements OnChanges, AfterViewInit
{
@Input() columnsTemplate: TemplateRef<any>;
@Input() columns: number;
@Input() items: any[] = [];
distributedColumns: any[] = [];
/**
* Constructor
*/
constructor(private _fuseMediaWatcherService: FuseMediaWatcherService)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On changes
*
* @param changes
*/
ngOnChanges(changes: SimpleChanges): void
{
// Columns
if ( 'columns' in changes )
{
// Distribute the items
this._distributeItems();
}
// Items
if ( 'items' in changes )
{
// Distribute the items
this._distributeItems();
}
}
/**
* After view init
*/
ngAfterViewInit(): void
{
// Distribute the items for the first time
this._distributeItems();
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Distribute items into columns
*/
private _distributeItems(): void
{
// Return an empty array if there are no items
if ( this.items.length === 0 )
{
this.distributedColumns = [];
return;
}
// Prepare the distributed columns array
this.distributedColumns = Array.from(Array(this.columns), item => ({items: []}));
// Distribute the items to columns
for ( let i = 0; i < this.items.length; i++ )
{
this.distributedColumns[i % this.columns].items.push(this.items[i]);
}
}
}

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FuseMasonryComponent } from '@fuse/components/masonry/masonry.component';
@NgModule({
declarations: [
FuseMasonryComponent
],
imports : [
CommonModule
],
exports : [
FuseMasonryComponent
]
})
export class FuseMasonryModule
{
}

View File

@@ -0,0 +1,2 @@
export * from '@fuse/components/masonry/masonry.component';
export * from '@fuse/components/masonry/masonry.module';

View File

@@ -36,7 +36,6 @@
.mat-tab-body-content {
.fuse-highlight {
margin: -24px;
pre {
margin: 0;

View File

@@ -72,13 +72,17 @@ function generateThemesObject(themes)
return _.map(_.cloneDeep(themes), (value, key) =>
{
const theme = normalizeTheme(value);
const primary = (theme && theme.primary && theme.primary.DEFAULT) ? theme.primary.DEFAULT : normalizedDefaultTheme.primary.DEFAULT;
const accent = (theme && theme.accent && theme.accent.DEFAULT) ? theme.accent.DEFAULT : normalizedDefaultTheme.accent.DEFAULT;
const warn = (theme && theme.warn && theme.warn.DEFAULT) ? theme.warn.DEFAULT : normalizedDefaultTheme.warn.DEFAULT;
return _.fromPairs([
[
key,
{
primary: theme?.primary?.DEFAULT ?? normalizedDefaultTheme.primary.DEFAULT,
accent : theme?.accent?.DEFAULT ?? normalizedDefaultTheme.accent.DEFAULT,
warn : theme?.warn?.DEFAULT ?? normalizedDefaultTheme.warn.DEFAULT
primary,
accent,
warn
}
]
]);

View File

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

View File

@@ -1,4 +1,4 @@
import { Version } from '@fuse/version/version';
const __FUSE_VERSION__ = '12.0.0';
const __FUSE_VERSION__ = '12.3.0';
export const FUSE_VERSION = new Version(__FUSE_VERSION__).full;

View File

@@ -82,12 +82,15 @@ 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: 'chat', loadChildren: () => import('app/modules/admin/apps/chat/chat.module').then(m => m.ChatModule)},
{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)},
{path: 'file-manager', loadChildren: () => import('app/modules/admin/apps/file-manager/file-manager.module').then(m => m.FileManagerModule)},
{path: 'help-center', loadChildren: () => import('app/modules/admin/apps/help-center/help-center.module').then(m => m.HelpCenterModule)},
{path: 'mailbox', loadChildren: () => import('app/modules/admin/apps/mailbox/mailbox.module').then(m => m.MailboxModule)},
{path: 'notes', loadChildren: () => import('app/modules/admin/apps/notes/notes.module').then(m => m.NotesModule)},
{path: 'tasks', loadChildren: () => import('app/modules/admin/apps/tasks/tasks.module').then(m => m.TasksModule)},
]},

View File

@@ -38,6 +38,7 @@ export class CoreModule
// Register icon sets
this._matIconRegistry.addSvgIconSet(this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/material-twotone.svg'));
this._matIconRegistry.addSvgIconSetInNamespace('mat_outline', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/material-outline.svg'));
this._matIconRegistry.addSvgIconSetInNamespace('mat_solid', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/material-solid.svg'));
this._matIconRegistry.addSvgIconSetInNamespace('iconsmind', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/iconsmind.svg'));
this._matIconRegistry.addSvgIconSetInNamespace('feather', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/feather.svg'));
this._matIconRegistry.addSvgIconSetInNamespace('heroicons_outline', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/heroicons-outline.svg'));

View File

@@ -63,6 +63,7 @@
</ng-container>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@@ -25,6 +26,7 @@ import { CenteredLayoutComponent } from 'app/layout/layouts/horizontal/centered/
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@@ -46,6 +46,7 @@
</ng-container>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@@ -25,6 +26,7 @@ import { EnterpriseLayoutComponent } from 'app/layout/layouts/horizontal/enterpr
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@@ -52,6 +52,7 @@
</ng-container>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@@ -25,6 +26,7 @@ import { MaterialLayoutComponent } from 'app/layout/layouts/horizontal/material/
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@@ -55,6 +55,7 @@
</ng-container>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@@ -25,6 +26,7 @@ import { ModernLayoutComponent } from 'app/layout/layouts/horizontal/modern/mode
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@@ -36,6 +36,7 @@
</button>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@@ -25,6 +26,7 @@ import { ClassicLayoutComponent } from 'app/layout/layouts/vertical/classic/clas
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@@ -66,6 +66,7 @@
</button>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>
@@ -80,8 +81,8 @@
</div>
<!-- Footer -->
<div class="relative flex flex-0 items-center justify-start w-full h-14 px-4 md:px-6 z-49 border-t bg-card dark:bg-transparent print:hidden">
<!--<div class="relative flex flex-0 items-center justify-start w-full h-14 px-4 md:px-6 z-49 border-t bg-card dark:bg-transparent print:hidden">
<span class="font-medium text-secondary">Fuse &copy; {{currentYear}}</span>
</div>
</div>-->
</div>

View File

@@ -6,6 +6,7 @@ import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { FuseFullscreenModule } from '@fuse/components/fullscreen/fullscreen.module';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
import { SearchModule } from 'app/layout/common/search/search.module';
@@ -25,6 +26,7 @@ import { ClassyLayoutComponent } from 'app/layout/layouts/vertical/classy/classy
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@@ -31,6 +31,7 @@
</button>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@@ -25,6 +26,7 @@ import { CompactLayoutComponent } from 'app/layout/layouts/vertical/compact/comp
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@@ -40,6 +40,7 @@
</div>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@@ -25,6 +26,7 @@ import { DenseLayoutComponent } from 'app/layout/layouts/vertical/dense/dense.co
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@@ -45,6 +45,7 @@
</button>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@@ -25,6 +26,7 @@ import { FuturisticLayoutComponent } from 'app/layout/layouts/vertical/futuristi
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@@ -32,6 +32,7 @@
</button>
<!-- Components -->
<div class="flex items-center pl-2 ml-auto space-x-2">
<fuse-fullscreen></fuse-fullscreen>
<search [appearance]="'bar'"></search>
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
<messages [messages]="data.messages"></messages>

View File

@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
import { FuseNavigationModule } from '@fuse/components/navigation';
import { MessagesModule } from 'app/layout/common/messages/messages.module';
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
@@ -25,6 +26,7 @@ import { ThinLayoutComponent } from 'app/layout/layouts/vertical/thin/thin.compo
MatDividerModule,
MatIconModule,
MatMenuModule,
FuseFullscreenModule,
FuseNavigationModule,
MessagesModule,
NotificationsModule,

View File

@@ -0,0 +1,89 @@
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);
if ( course )
{
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

@@ -0,0 +1,167 @@
import { Injectable } from '@angular/core';
import { assign, cloneDeep, omit } from 'lodash-es';
import { FuseMockApiService } from '@fuse/lib/mock-api';
import { chats as chatsData, contacts as contactsData, messages as messagesData, profile as profileData } from 'app/mock-api/apps/chat/data';
@Injectable({
providedIn: 'root'
})
export class ChatMockApi
{
private _chats: any[] = chatsData;
private _contacts: any[] = contactsData;
private _messages: any[] = messagesData;
private _profile: any = profileData;
/**
* Constructor
*/
constructor(private _fuseMockApiService: FuseMockApiService)
{
// Register Mock API handlers
this.registerHandlers();
// Modify the chats array to attach certain data to it
this._chats = this._chats.map((chat) => ({
...chat,
// Get the actual contact object from the id and attach it to the chat
contact: this._contacts.find((contact) => contact.id === chat.contactId),
// Since we use same set of messages on all chats, we assign them here.
messages: this._messages.map((message) => ({
...message,
chatId : chat.id,
contactId: message.contactId === 'me' ? this._profile.id : chat.contactId,
isMine : message.contactId === 'me'
}))
}));
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Register Mock API handlers
*/
registerHandlers(): void
{
// -----------------------------------------------------------------------------------------------------
// @ Chats - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/chat/chats')
.reply(() => {
// Clone the chats
const chats = cloneDeep(this._chats);
// Return the response
return [200, chats];
});
// -----------------------------------------------------------------------------------------------------
// @ Chat - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/chat/chat')
.reply(({request}) => {
// Get the chat id
const id = request.params.get('id');
// Clone the chats
const chats = cloneDeep(this._chats);
// Find the chat we need
const chat = chats.find((item) => item.id === id);
// Return the response
return [200, chat];
});
// -----------------------------------------------------------------------------------------------------
// @ Chat - PATCH
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onPatch('api/apps/chat/chat')
.reply(({request}) => {
// Get the id and chat
const id = request.body.id;
const chat = cloneDeep(request.body.chat);
// Prepare the updated chat
let updatedChat = null;
// Find the chat and update it
this._chats.forEach((item, index, chats) => {
if ( item.id === id )
{
// Update the chat
chats[index] = assign({}, chats[index], chat);
// Store the updated chat
updatedChat = chats[index];
}
});
// Return the response
return [200, updatedChat];
});
// -----------------------------------------------------------------------------------------------------
// @ Contacts - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/chat/contacts')
.reply(() => {
// Clone the contacts
let contacts = cloneDeep(this._contacts);
// Sort the contacts by the name field by default
contacts.sort((a, b) => a.name.localeCompare(b.name));
// Omit details and attachments from contacts
contacts = contacts.map((contact) => omit(contact, ['details', 'attachments']));
// Return the response
return [200, contacts];
});
// -----------------------------------------------------------------------------------------------------
// @ Contact Details - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/chat/contact')
.reply(({request}) => {
// Get the contact id
const id = request.params.get('id');
// Clone the contacts
const contacts = cloneDeep(this._contacts);
// Find the contact
const contact = contacts.find((item) => item.id === id);
// Return the response
return [200, contact];
});
// -----------------------------------------------------------------------------------------------------
// @ Profile - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/chat/profile')
.reply(() => {
// Clone the profile
const profile = cloneDeep(this._profile);
// Return the response
return [200, profile];
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,264 @@
import { Injectable } from '@angular/core';
import { cloneDeep } from 'lodash-es';
import { FuseMockApiService } from '@fuse/lib/mock-api/mock-api.service';
import { labels as labelsData, notes as notesData } from 'app/mock-api/apps/notes/data';
import { FuseMockApiUtils } from '@fuse/lib/mock-api';
@Injectable({
providedIn: 'root'
})
export class NotesMockApi
{
private _labels: any[] = labelsData;
private _notes: any[] = notesData;
/**
* Constructor
*/
constructor(private _fuseMockApiService: FuseMockApiService)
{
// Register Mock API handlers
this.registerHandlers();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Register Mock API handlers
*/
registerHandlers(): void
{
// -----------------------------------------------------------------------------------------------------
// @ Labels - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/notes/labels')
.reply(() => {
return [
200,
cloneDeep(this._labels)
];
});
// -----------------------------------------------------------------------------------------------------
// @ Labels - POST
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onPost('api/apps/notes/labels')
.reply(({request}) => {
// Create a new label
const label = {
id : FuseMockApiUtils.guid(),
title: request.body.title
};
// Update the labels
this._labels.push(label);
return [
200,
cloneDeep(this._labels)
];
});
// -----------------------------------------------------------------------------------------------------
// @ Labels - PATCH
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onPatch('api/apps/notes/labels')
.reply(({request}) => {
// Get label
const updatedLabel = request.body.label;
// Update the label
this._labels = this._labels.map((label) => {
if ( label.id === updatedLabel.id )
{
return {
...label,
title: updatedLabel.title
};
}
return label;
});
return [
200,
cloneDeep(this._labels)
];
});
// -----------------------------------------------------------------------------------------------------
// @ Labels - DELETE
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onDelete('api/apps/notes/labels')
.reply(({request}) => {
// Get label id
const id = request.params.get('id');
// Delete the label
this._labels = this._labels.filter((label) => label.id !== id);
// Go through notes and delete the label
this._notes = this._notes.map((note) => ({
...note,
labels: note.labels.filter((item) => item !== id)
}));
return [
200,
cloneDeep(this._labels)
];
});
// -----------------------------------------------------------------------------------------------------
// @ Note Tasks - POST
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onPost('api/apps/notes/tasks')
.reply(({request}) => {
// Get note and task
let updatedNote = request.body.note;
const task = request.body.task;
// Update the note
this._notes = this._notes.map((note) => {
if ( note.id === updatedNote.id )
{
// Update the tasks
if ( !note.tasks )
{
note.tasks = [];
}
note.tasks.push({
id : FuseMockApiUtils.guid(),
content : task,
completed: false
});
// Update the updatedNote with the new task
updatedNote = cloneDeep(note);
return {
...note
};
}
return note;
});
return [
200,
updatedNote
];
});
// -----------------------------------------------------------------------------------------------------
// @ Notes - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/apps/notes/all')
.reply(() => {
// Clone the labels and notes
const labels = cloneDeep(this._labels);
let notes = cloneDeep(this._notes);
// Attach the labels to the notes
notes = notes.map((note) => (
{
...note,
labels: note.labels.map((labelId) => labels.find((label) => label.id === labelId))
}
));
return [
200,
notes
];
});
// -----------------------------------------------------------------------------------------------------
// @ Notes - POST
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onPost('api/apps/notes')
.reply(({request}) => {
// Get note
const note = request.body.note;
// Add an id
note.id = FuseMockApiUtils.guid();
// Push the note
this._notes.push(note);
return [
200,
note
];
});
// -----------------------------------------------------------------------------------------------------
// @ Notes - PATCH
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onPatch('api/apps/notes')
.reply(({request}) => {
// Get note
const updatedNote = request.body.updatedNote;
// Update the note
this._notes = this._notes.map((note) => {
if ( note.id === updatedNote.id )
{
return {
...updatedNote
};
}
return note;
});
return [
200,
updatedNote
];
});
// -----------------------------------------------------------------------------------------------------
// @ Notes - DELETE
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onDelete('api/apps/notes')
.reply(({request}) => {
// Get the id
const id = request.params.get('id');
// Find the note and delete it
this._notes.forEach((item, index) => {
if ( item.id === id )
{
this._notes.splice(index, 1);
}
});
// Return the response
return [200, true];
});
}
}

View File

@@ -0,0 +1,314 @@
/* tslint:disable:max-line-length */
import moment from 'moment';
export const labels = [
{
id : 'f47c92e5-20b9-44d9-917f-9ff4ad25dfd0',
title: 'Family'
},
{
id : 'e2f749f5-41ed-49d0-a92a-1c83d879e371',
title: 'Work'
},
{
id : 'b1cde9ee-e54d-4142-ad8b-cf55dafc9528',
title: 'Tasks'
},
{
id : '6c288794-47eb-4605-8bdf-785b61a449d3',
title: 'Priority'
},
{
id : 'bbc73458-940b-421c-8d5f-8dcd23a9b0d6',
title: 'Personal'
},
{
id : '2dc11344-3507-48e0-83d6-1c047107f052',
title: 'Friends'
}
];
export const notes = [
{
id : '8f011ac5-b71c-4cd7-a317-857dcd7d85e0',
title : '',
content : 'Find a new company name',
tasks : null,
image : null,
reminder : null,
labels : ['e2f749f5-41ed-49d0-a92a-1c83d879e371'],
archived : false,
createdAt: moment().hour(10).minute(19).subtract(98, 'day').toISOString(),
updatedAt: null
},
{
id : 'ced0a1ce-051d-41a3-b080-e2161e4ae621',
title : '',
content : 'Send the photos of last summer to John',
tasks : null,
image : 'assets/images/cards/14-640x480.jpg',
reminder : null,
labels : [
'bbc73458-940b-421c-8d5f-8dcd23a9b0d6',
'b1cde9ee-e54d-4142-ad8b-cf55dafc9528'
],
archived : false,
createdAt: moment().hour(15).minute(37).subtract(80, 'day').toISOString(),
updatedAt: null
},
{
id : 'd3ac02a9-86e4-4187-bbd7-2c965518b3a3',
title : '',
content : 'Update the design of the theme',
tasks : null,
image : null,
reminder : null,
labels : ['6c288794-47eb-4605-8bdf-785b61a449d3'],
archived : false,
createdAt: moment().hour(19).minute(27).subtract(74, 'day').toISOString(),
updatedAt: moment().hour(15).minute(36).subtract(50, 'day').toISOString()
},
{
id : '89861bd4-0144-4bb4-8b39-332ca10371d5',
title : '',
content : 'Theming support for all apps',
tasks : null,
image : null,
reminder : moment().hour(12).minute(34).add(50, 'day').toISOString(),
labels : ['e2f749f5-41ed-49d0-a92a-1c83d879e371'],
archived : false,
createdAt: moment().hour(12).minute(34).subtract(59, 'day').toISOString(),
updatedAt: null
},
{
id : 'ffd20f3c-2d43-4c6b-8021-278032fc9e92',
title : 'Gift Ideas',
content : 'Stephanie\'s birthday is coming and I need to pick a present for her. Take a look at the below list and buy one of them (or all of them)',
tasks : [
{
id : '330a924f-fb51-48f6-a374-1532b1dd353d',
content : 'Scarf',
completed: false
},
{
id : '781855a6-2ad2-4df4-b0af-c3cb5f302b40',
content : 'A new bike helmet',
completed: true
},
{
id : 'bcb8923b-33cd-42c2-9203-170994fa24f5',
content : 'Necklace',
completed: false
},
{
id : '726bdf6e-5cd7-408a-9a4f-0d7bb98c1c4b',
content : 'Flowers',
completed: false
}
],
image : null,
reminder : null,
labels : ['f47c92e5-20b9-44d9-917f-9ff4ad25dfd0'],
archived : false,
createdAt: moment().hour(16).minute(4).subtract(47, 'day').toISOString(),
updatedAt: null
},
{
id : '71d223bb-abab-4183-8919-cd3600a950b4',
title : 'Shopping list',
content : '',
tasks : [
{
id : 'e3cbc986-641c-4448-bc26-7ecfa0549c22',
content : 'Bread',
completed: true
},
{
id : '34013111-ab2c-4b2f-9352-d2ae282f57d3',
content : 'Milk',
completed: false
},
{
id : '0fbdea82-cc79-4433-8ee4-54fd542c380d',
content : 'Onions',
completed: false
},
{
id : '66490222-743e-4262-ac91-773fcd98a237',
content : 'Coffee',
completed: true
},
{
id : 'ab367215-d06a-48b0-a7b8-e161a63b07bd',
content : 'Toilet Paper',
completed: true
}
],
image : null,
reminder : moment().hour(10).minute(44).subtract(35, 'day').toISOString(),
labels : ['b1cde9ee-e54d-4142-ad8b-cf55dafc9528'],
archived : false,
createdAt: moment().hour(10).minute(44).subtract(35, 'day').toISOString(),
updatedAt: null
},
{
id : '11fbeb98-ae5e-41ad-bed6-330886fd7906',
title : 'Keynote Schedule',
content : '',
tasks : [
{
id : '2711bac1-7d8a-443a-a4fe-506ef51d3fcb',
content : 'Breakfast',
completed: true
},
{
id : 'e3a2d675-a3e5-4cef-9205-feeccaf949d7',
content : 'Opening ceremony',
completed: true
},
{
id : '7a721b6d-9d85-48e0-b6c3-f927079af582',
content : 'Talk 1: How we did it!',
completed: true
},
{
id : 'bdb4d5cd-5bb8-45e2-9186-abfd8307e429',
content : 'Talk 2: How can you do it!',
completed: false
},
{
id : 'c8293bb4-8ab4-4310-bbc2-52ecf8ec0c54',
content : 'Lunch break',
completed: false
}
],
image : null,
reminder : moment().hour(11).minute(27).subtract(14, 'day').toISOString(),
labels : [
'b1cde9ee-e54d-4142-ad8b-cf55dafc9528',
'e2f749f5-41ed-49d0-a92a-1c83d879e371'
],
archived : false,
createdAt: moment().hour(11).minute(27).subtract(24, 'day').toISOString(),
updatedAt: null
},
{
id : 'd46dee8b-8761-4b6d-a1df-449d6e6feb6a',
title : '',
content : 'Organize the dad\'s surprise retirement party',
tasks : null,
image : null,
reminder : moment().hour(14).minute(56).subtract(25, 'day').toISOString(),
labels : ['f47c92e5-20b9-44d9-917f-9ff4ad25dfd0'],
archived : false,
createdAt: moment().hour(14).minute(56).subtract(20, 'day').toISOString(),
updatedAt: null
},
{
id : '6bc9f002-1675-417c-93c4-308fba39023e',
title : 'Plan the road trip',
content : '',
tasks : null,
image : 'assets/images/cards/17-640x480.jpg',
reminder : null,
labels : [
'2dc11344-3507-48e0-83d6-1c047107f052',
'b1cde9ee-e54d-4142-ad8b-cf55dafc9528'
],
archived : false,
createdAt: moment().hour(9).minute(32).subtract(15, 'day').toISOString(),
updatedAt: moment().hour(17).minute(6).subtract(12, 'day').toISOString()
},
{
id : '15188348-78aa-4ed6-b5c2-028a214ba987',
title : 'Office Address',
content : '933 8th Street Stamford, CT 06902',
tasks : null,
image : null,
reminder : null,
labels : ['e2f749f5-41ed-49d0-a92a-1c83d879e371'],
archived : false,
createdAt: moment().hour(20).minute(5).subtract(12, 'day').toISOString(),
updatedAt: null
},
{
id : '1dbfc685-1a0a-4070-9ca7-ed896c523037',
title : 'Tasks',
content : '',
tasks : [
{
id : '004638bf-3ee6-47a5-891c-3be7b9f3df09',
content : 'Wash the dishes',
completed: true
},
{
id : '86e6820b-1ae3-4c14-a13e-35605a0d654b',
content : 'Walk the dog',
completed: false
}
],
image : null,
reminder : moment().hour(13).minute(43).subtract(2, 'day').toISOString(),
labels : ['bbc73458-940b-421c-8d5f-8dcd23a9b0d6'],
archived : false,
createdAt: moment().hour(13).minute(43).subtract(7, 'day').toISOString(),
updatedAt: null
},
{
id : '49548409-90a3-44d4-9a9a-f5af75aa9a66',
title : '',
content : 'Dinner with parents',
tasks : null,
image : null,
reminder : null,
labels : [
'f47c92e5-20b9-44d9-917f-9ff4ad25dfd0',
'6c288794-47eb-4605-8bdf-785b61a449d3'
],
archived : false,
createdAt: moment().hour(7).minute(12).subtract(2, 'day').toISOString(),
updatedAt: null
},
{
id : 'c6d13a35-500d-4491-a3f3-6ca05d6632d3',
title : '',
content : 'Re-fill the medicine cabinet',
tasks : null,
image : null,
reminder : null,
labels : [
'bbc73458-940b-421c-8d5f-8dcd23a9b0d6',
'6c288794-47eb-4605-8bdf-785b61a449d3'
],
archived : true,
createdAt: moment().hour(17).minute(14).subtract(100, 'day').toISOString(),
updatedAt: null
},
{
id : 'c6d13a35-500d-4491-a3f3-6ca05d6632d3',
title : '',
content : 'Update the icons pack',
tasks : null,
image : null,
reminder : null,
labels : ['e2f749f5-41ed-49d0-a92a-1c83d879e371'],
archived : true,
createdAt: moment().hour(10).minute(29).subtract(85, 'day').toISOString(),
updatedAt: null
},
{
id : '46214383-f8e7-44da-aa2e-0b685e0c5027',
title : 'Team Meeting',
content : 'Talk about the future of the web apps',
tasks : null,
image : null,
reminder : null,
labels : [
'e2f749f5-41ed-49d0-a92a-1c83d879e371',
'b1cde9ee-e54d-4142-ad8b-cf55dafc9528'
],
archived : true,
createdAt: moment().hour(15).minute(30).subtract(69, 'day').toISOString(),
updatedAt: null
}
];

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',
@@ -41,6 +47,13 @@ export const defaultNavigation: FuseNavigationItem[] = [
icon : 'heroicons_outline:calendar',
link : '/apps/calendar'
},
{
id : 'apps.chat',
title: 'Chat',
type : 'basic',
icon : 'heroicons_outline:chat-alt',
link : '/apps/chat'
},
{
id : 'apps.contacts',
title: 'Contacts',
@@ -114,6 +127,13 @@ export const defaultNavigation: FuseNavigationItem[] = [
classes: 'px-2 bg-pink-600 text-white rounded-full'
}
},
{
id : 'apps.notes',
title: 'Notes',
type : 'basic',
icon : 'heroicons_outline:pencil-alt',
link : '/apps/notes'
},
{
id : 'apps.tasks',
title: 'Tasks',
@@ -727,6 +747,12 @@ export const defaultNavigation: FuseNavigationItem[] = [
type : 'basic',
link : '/ui/icons/heroicons-solid'
},
{
id : 'user-interface.icons.material-twotone',
title: 'Material Twotone',
type : 'basic',
link : '/ui/icons/material-twotone'
},
{
id : 'user-interface.icons.material-outline',
title: 'Material Outline',
@@ -734,10 +760,10 @@ export const defaultNavigation: FuseNavigationItem[] = [
link : '/ui/icons/material-outline'
},
{
id : 'user-interface.icons.material-twotone',
title: 'Material Twotone',
id : 'user-interface.icons.material-solid',
title: 'Material Solid',
type : 'basic',
link : '/ui/icons/material-twotone'
link : '/ui/icons/material-solid'
},
{
id : 'user-interface.icons.iconsmind',
@@ -887,7 +913,7 @@ export const defaultNavigation: FuseNavigationItem[] = [
icon : 'heroicons_outline:speakerphone',
link : '/docs/changelog',
badge: {
title : '12.0.0',
title : '12.3.0',
classes: 'px-2 bg-yellow-300 text-black rounded-full'
}
},
@@ -1129,6 +1155,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',
@@ -1136,6 +1169,13 @@ export const futuristicNavigation: FuseNavigationItem[] = [
icon : 'heroicons_outline:calendar',
link : '/apps/calendar'
},
{
id : 'apps.chat',
title: 'Chat',
type : 'basic',
icon : 'heroicons_outline:chat-alt',
link : '/apps/chat'
},
{
id : 'apps.contacts',
title: 'Contacts',
@@ -1209,6 +1249,13 @@ export const futuristicNavigation: FuseNavigationItem[] = [
classes: 'px-2 bg-black bg-opacity-25 text-white rounded-full'
}
},
{
id : 'apps.notes',
title: 'Notes',
type : 'basic',
icon : 'heroicons_outline:pencil-alt',
link : '/apps/notes'
},
{
id : 'apps.tasks',
title: 'Tasks',

View File

@@ -1,6 +1,8 @@
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';
import { ChatMockApi } from 'app/mock-api/apps/chat/api';
import { ContactsMockApi } from 'app/mock-api/apps/contacts/api';
import { ECommerceInventoryMockApi } from 'app/mock-api/apps/ecommerce/inventory/api';
import { FileManagerMockApi } from 'app/mock-api/apps/file-manager/api';
@@ -9,6 +11,7 @@ import { IconsMockApi } from 'app/mock-api/ui/icons/api';
import { MailboxMockApi } from 'app/mock-api/apps/mailbox/api';
import { MessagesMockApi } from 'app/mock-api/common/messages/api';
import { NavigationMockApi } from 'app/mock-api/common/navigation/api';
import { NotesMockApi } from 'app/mock-api/apps/notes/api';
import { NotificationsMockApi } from 'app/mock-api/common/notifications/api';
import { ProjectMockApi } from 'app/mock-api/dashboards/project/api';
import { SearchMockApi } from 'app/mock-api/common/search/api';
@@ -17,9 +20,11 @@ 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,
ChatMockApi,
ContactsMockApi,
ECommerceInventoryMockApi,
FileManagerMockApi,
@@ -28,6 +33,7 @@ export const mockApiServices = [
MailboxMockApi,
MessagesMockApi,
NavigationMockApi,
NotesMockApi,
NotificationsMockApi,
ProjectMockApi,
SearchMockApi,

View File

@@ -91,6 +91,21 @@ export class IconsMockApi
}
]);
// -----------------------------------------------------------------------------------------------------
// @ Material solid icons - GET
// -----------------------------------------------------------------------------------------------------
this._fuseMockApiService
.onGet('api/ui/icons/material-solid')
.reply(() => [
200,
{
namespace: 'mat_solid',
name : 'Material Solid',
grid : 6,
list : cloneDeep(this._material)
}
]);
// -----------------------------------------------------------------------------------------------------
// @ Material outline icons - GET
// -----------------------------------------------------------------------------------------------------

View File

@@ -1,6 +1,6 @@
/* tslint:disable:max-line-length */
// 20210326 - 1777 icons
// Updated at: 20210425 - 1792 icons
export const material = [
'10k',
'10mp',
@@ -100,6 +100,7 @@ export const material = [
'addchart',
'adjust',
'admin_panel_settings',
'ads_click',
'agriculture',
'air',
'airline_seat_flat',
@@ -146,6 +147,7 @@ export const material = [
'apps',
'architecture',
'archive',
'area_chart',
'arrow_back',
'arrow_back_ios',
'arrow_back_ios_new',
@@ -195,6 +197,7 @@ export const material = [
'autorenew',
'av_timer',
'baby_changing_station',
'back_hand',
'backpack',
'backspace',
'backup',
@@ -386,6 +389,7 @@ export const material = [
'compare',
'compare_arrows',
'compass_calibration',
'compost',
'compress',
'computer',
'confirmation_number',
@@ -429,10 +433,12 @@ export const material = [
'crop_portrait',
'crop_rotate',
'crop_square',
'cruelty_free',
'dangerous',
'dark_mode',
'dashboard',
'dashboard_customize',
'data_exploration',
'data_saver_off',
'data_saver_on',
'data_usage',
@@ -482,6 +488,7 @@ export const material = [
'directions_walk',
'dirty_lens',
'disabled_by_default',
'disabled_visible',
'disc_full',
'dns',
'do_disturb',
@@ -521,6 +528,7 @@ export const material = [
'draw',
'drive_eta',
'drive_file_move',
'drive_file_move_rtl',
'drive_file_rename_outline',
'drive_folder_upload',
'dry',
@@ -533,7 +541,6 @@ export const material = [
'earbuds',
'earbuds_battery',
'east',
'eco',
'edgesensor_high',
'edgesensor_low',
'edit',
@@ -715,6 +722,7 @@ export const material = [
'foundation',
'free_breakfast',
'free_cancellation',
'front_hand',
'fullscreen',
'fullscreen_exit',
'functions',
@@ -829,6 +837,7 @@ export const material = [
'import_export',
'important_devices',
'inbox',
'incomplete_circle',
'indeterminate_check_box',
'info',
'input',
@@ -1215,6 +1224,8 @@ export const material = [
'pie_chart_outline',
'pin',
'pin_drop',
'pin_end',
'pin_invoke',
'pivot_table_chart',
'place',
'plagiarism',
@@ -1289,6 +1300,7 @@ export const material = [
'recent_actors',
'recommend',
'record_voice_over',
'recycling',
'redeem',
'redo',
'reduce_capacity',
@@ -1631,6 +1643,7 @@ export const material = [
'timer_3',
'timer_3_select',
'timer_off',
'tips_and_updates',
'title',
'toc',
'today',
@@ -1737,8 +1750,10 @@ export const material = [
'watch_later',
'water',
'water_damage',
'water_drop',
'waterfall_chart',
'waves',
'waving_hand',
'wb_auto',
'wb_cloudy',
'wb_incandescent',
@@ -4152,87 +4167,91 @@ export const feather = [
'zoom-in',
'zoom-out'
];
// heroicons v0.4.2
// heroicons v1.0.1 - 230 icons
export const heroicons = [
'academic-cap',
'annotation',
'arrow-circle-down',
'adjustments',
'archive',
'arrow-circle-down',
'arrow-circle-left',
'arrow-circle-right',
'archive',
'arrow-circle-up',
'arrow-down',
'arrow-narrow-down',
'arrow-left',
'arrow-narrow-down',
'arrow-narrow-left',
'arrow-narrow-right',
'arrow-right',
'arrow-narrow-up',
'arrow-right',
'arrow-sm-left',
'arrow-sm-down',
'arrow-sm-right',
'arrow-sm-up',
'arrow-up',
'at-symbol',
'arrows-expand',
'backspace',
'at-symbol',
'badge-check',
'backspace',
'ban',
'beaker',
'bell',
'book-open',
'bookmark-alt',
'bookmark',
'calendar',
'briefcase',
'calculator',
'cake',
'calculator',
'calendar',
'camera',
'cash',
'chart-bar',
'chart-pie',
'chart-square-bar',
'chat-alt-2',
'chat-alt',
'chat',
'check-circle',
'chat-alt',
'check',
'chevron-double-down',
'chevron-double-left',
'chevron-down',
'chevron-double-up',
'chevron-left',
'chevron-double-right',
'chevron-down',
'chevron-left',
'chevron-right',
'chevron-up',
'chip',
'clipboard-check',
'clipboard-copy',
'clipboard-list',
'clock',
'cloud-upload',
'clipboard',
'clock',
'cloud-download',
'code',
'cloud-upload',
'cloud',
'code',
'cog',
'collection',
'color-swatch',
'credit-card',
'color-swatch',
'cube-transparent',
'cube',
'currency-bangladeshi',
'currency-dollar',
'currency-pound',
'currency-euro',
'currency-pound',
'currency-rupee',
'cursor-click',
'currency-yen',
'cursor-click',
'database',
'desktop-computer',
'device-mobile',
'device-tablet',
'document-download',
'document-add',
'document-remove',
'document-download',
'document-duplicate',
'document-remove',
'document-report',
'document-search',
'document-text',
@@ -4241,81 +4260,81 @@ export const heroicons = [
'dots-horizontal',
'dots-vertical',
'download',
'duplicate',
'emoji-happy',
'emoji-sad',
'duplicate',
'exclamation-circle',
'exclamation',
'external-link',
'eye-off',
'film',
'fast-forward',
'finger-print',
'eye',
'fast-forward',
'film',
'filter',
'finger-print',
'fire',
'flag',
'folder-add',
'folder-remove',
'folder-open',
'folder',
'folder-download',
'folder-open',
'folder-remove',
'folder',
'gift',
'globe-alt',
'hand',
'globe',
'hand',
'hashtag',
'identification',
'heart',
'home',
'identification',
'inbox-in',
'inbox',
'information-circle',
'key',
'library',
'light-bulb',
'lightning-bolt',
'library',
'link',
'location-marker',
'lock-closed',
'lock-open',
'logout',
'mail',
'login',
'map',
'logout',
'mail-open',
'mail',
'map',
'menu-alt-1',
'menu-alt-2',
'menu-alt-3',
'menu-alt-4',
'menu-alt-2',
'menu',
'microphone',
'minus-circle',
'minus-sm',
'minus',
'music-note',
'moon',
'music-note',
'newspaper',
'office-building',
'paper-clip',
'paper-airplane',
'paper-clip',
'pause',
'pencil-alt',
'pencil',
'phone-incoming',
'phone-missed-call',
'phone-outgoing',
'plus-circle',
'play',
'photograph',
'phone',
'play',
'plus-circle',
'plus-sm',
'plus',
'presentation-chart-bar',
'puzzle',
'presentation-chart-line',
'qrcode',
'printer',
'qrcode',
'puzzle',
'question-mark-circle',
'receipt-refund',
'receipt-tax',
@@ -4327,8 +4346,8 @@ export const heroicons = [
'save',
'scale',
'scissors',
'search',
'search-circle',
'search',
'selector',
'server',
'share',
@@ -4338,22 +4357,22 @@ export const heroicons = [
'shopping-cart',
'sort-ascending',
'sort-descending',
'sparkles',
'speakerphone',
'sparkles',
'star',
'status-offline',
'status-online',
'status-offline',
'stop',
'sun',
'support',
'switch-horizontal',
'switch-vertical',
'switch-horizontal',
'table',
'tag',
'template',
'thumb-down',
'terminal',
'thumb-up',
'thumb-down',
'ticket',
'translate',
'trash',
@@ -4362,22 +4381,22 @@ export const heroicons = [
'truck',
'upload',
'user-add',
'user-circle',
'user-group',
'user',
'user-circle',
'user-remove',
'user',
'users',
'variable',
'video-camera',
'view-boards',
'variable',
'view-grid-add',
'view-grid',
'view-list',
'volume-off',
'volume-up',
'wifi',
'volume-off',
'x-circle',
'x',
'zoom-in',
'zoom-out'
'wifi',
'zoom-out',
'x'
];

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,105 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { map, switchMap, 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(
map((course) => {
// Update the course
this._course.next(course);
// Return the course
return course;
}),
switchMap((course) => {
if ( !course )
{
return throwError('Could not found course with id of ' + id + '!');
}
return of(course);
})
);
}
}

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,200 @@
<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; trackBy: trackByFn">
<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; trackBy: trackByFn">
<mat-tab>
<ng-template matTabContent>
<div
class="prose prose-sm max-w-3xl mx-auto sm:my-2 lg:mt-4 p-6 sm:p-10 sm:py-12 rounded-2xl shadow overflow-hidden bg-card"
[innerHTML]="step.content"></div>
</ng-template>
</mat-tab>
</ng-container>
</mat-tab-group>
<!-- Navigation - Desktop -->
<div class="z-10 sticky hidden lg:flex bottom-4 p-4">
<div class="flex items-center justify-center mx-auto p-2 rounded-full shadow-lg bg-primary">
<button
class="flex-0"
mat-flat-button
[color]="'primary'"
(click)="goToPreviousStep()">
<mat-icon
class="mr-2"
[svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
<span class="mr-1">Prev</span>
</button>
<div class="flex items-center justify-center mx-2.5 font-medium leading-5 text-on-primary">
<span>{{currentStep + 1}}</span>
<span class="mx-0.5 text-hint">/</span>
<span>{{course.totalSteps}}</span>
</div>
<button
class="flex-0"
mat-flat-button
[color]="'primary'"
(click)="goToNextStep()">
<span class="ml-1">Next</span>
<mat-icon
class="ml-2"
[svgIcon]="'heroicons_outline:arrow-narrow-right'"></mat-icon>
</button>
</div>
</div>
</div>
<!-- Progress & Navigation - Mobile -->
<div class="lg:hidden flex items-center p-4 border-t bg-card">
<button
mat-icon-button
(click)="matDrawer.toggle()">
<mat-icon [svgIcon]="'heroicons_outline:view-list'"></mat-icon>
</button>
<div class="flex items-center justify-center ml-1 lg:ml-2 font-medium leading-5">
<span>{{currentStep + 1}}</span>
<span class="mx-0.5 text-hint">/</span>
<span>{{course.totalSteps}}</span>
</div>
<mat-progress-bar
class="flex-auto ml-6 rounded-full"
[value]="100 * (currentStep + 1) / course.totalSteps"></mat-progress-bar>
<button
class="ml-4"
mat-icon-button
(click)="goToPreviousStep()">
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
</button>
<button
class="ml-0.5"
mat-icon-button
(click)="goToNextStep()">
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-right'"></mat-icon>
</button>
</div>
</mat-drawer-content>
</mat-drawer-container>
</div>

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; trackBy: trackByFn">
<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; trackBy: trackByFn">
<!-- 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

@@ -0,0 +1,8 @@
<div class="absolute inset-0 flex flex-col min-w-0 overflow-hidden">
<!-- Main -->
<div class="flex flex-auto overflow-hidden">
<router-outlet></router-outlet>
</div>
</div>

View File

@@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
@Component({
selector : 'chat',
templateUrl : './chat.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChatComponent
{
/**
* 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 { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatSidenavModule } from '@angular/material/sidenav';
import { FuseAutogrowModule } from '@fuse/directives/autogrow';
import { SharedModule } from 'app/shared/shared.module';
import { chatRoutes } from 'app/modules/admin/apps/chat/chat.routing';
import { ChatComponent } from 'app/modules/admin/apps/chat/chat.component';
import { ChatsComponent } from 'app/modules/admin/apps/chat/chats/chats.component';
import { ContactInfoComponent } from 'app/modules/admin/apps/chat/contact-info/contact-info.component';
import { ConversationComponent } from 'app/modules/admin/apps/chat/conversation/conversation.component';
import { NewChatComponent } from 'app/modules/admin/apps/chat/new-chat/new-chat.component';
import { ProfileComponent } from 'app/modules/admin/apps/chat/profile/profile.component';
@NgModule({
declarations: [
ChatComponent,
ChatsComponent,
ContactInfoComponent,
ConversationComponent,
NewChatComponent,
ProfileComponent
],
imports: [
RouterModule.forChild(chatRoutes),
MatButtonModule,
MatCheckboxModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatMenuModule,
MatSidenavModule,
FuseAutogrowModule,
SharedModule,
]
})
export class ChatModule
{
}

View File

@@ -0,0 +1,147 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ChatService } from 'app/modules/admin/apps/chat/chat.service';
import { Chat, Contact, Profile } from 'app/modules/admin/apps/chat/chat.types';
@Injectable({
providedIn: 'root'
})
export class ChatChatsResolver implements Resolve<any>
{
/**
* Constructor
*/
constructor(
private _chatService: ChatService,
private _router: Router
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resolver
*
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Chat[]> | any
{
return this._chatService.getChats();
}
}
@Injectable({
providedIn: 'root'
})
export class ChatChatResolver implements Resolve<any>
{
/**
* Constructor
*/
constructor(
private _chatService: ChatService,
private _router: Router
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resolver
*
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Chat>
{
return this._chatService.getChatById(route.paramMap.get('id'))
.pipe(
// Error here means the requested chat 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);
})
);
}
}
@Injectable({
providedIn: 'root'
})
export class ChatContactsResolver implements Resolve<any>
{
/**
* Constructor
*/
constructor(
private _chatService: ChatService,
private _router: Router
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resolver
*
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Contact[]> | any
{
return this._chatService.getContacts();
}
}
@Injectable({
providedIn: 'root'
})
export class ChatProfileResolver implements Resolve<any>
{
/**
* Constructor
*/
constructor(
private _chatService: ChatService,
private _router: Router
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resolver
*
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Profile> | any
{
return this._chatService.getProfile();
}
}

View File

@@ -0,0 +1,37 @@
import { Route } from '@angular/router';
import { ChatChatResolver, ChatChatsResolver, ChatContactsResolver, ChatProfileResolver } from 'app/modules/admin/apps/chat/chat.resolvers';
import { ChatComponent } from 'app/modules/admin/apps/chat/chat.component';
import { ChatsComponent } from 'app/modules/admin/apps/chat/chats/chats.component';
import { ConversationComponent } from 'app/modules/admin/apps/chat/conversation/conversation.component';
export const chatRoutes: Route[] = [
{
path : '',
component: ChatComponent,
resolve : {
chats : ChatChatsResolver,
contacts: ChatContactsResolver,
profile : ChatProfileResolver
},
children : [
{
path : '',
component: ChatsComponent,
children : [
{
path : '',
component: ConversationComponent,
children : [
{
path : ':id',
resolve: {
conversation: ChatChatResolver
}
}
]
}
]
}
]
}
];

View File

@@ -0,0 +1,202 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { Chat, Contact, Profile } from 'app/modules/admin/apps/chat/chat.types';
@Injectable({
providedIn: 'root'
})
export class ChatService
{
private _chat: BehaviorSubject<Chat> = new BehaviorSubject(null);
private _chats: BehaviorSubject<Chat[]> = new BehaviorSubject(null);
private _contact: BehaviorSubject<Contact> = new BehaviorSubject(null);
private _contacts: BehaviorSubject<Contact[]> = new BehaviorSubject(null);
private _profile: BehaviorSubject<Profile> = new BehaviorSubject(null);
/**
* Constructor
*/
constructor(private _httpClient: HttpClient)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Getter for chat
*/
get chat$(): Observable<Chat>
{
return this._chat.asObservable();
}
/**
* Getter for chats
*/
get chats$(): Observable<Chat[]>
{
return this._chats.asObservable();
}
/**
* Getter for contact
*/
get contact$(): Observable<Contact>
{
return this._contact.asObservable();
}
/**
* Getter for contacts
*/
get contacts$(): Observable<Contact[]>
{
return this._contacts.asObservable();
}
/**
* Getter for profile
*/
get profile$(): Observable<Profile>
{
return this._profile.asObservable();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Get chats
*/
getChats(): Observable<any>
{
return this._httpClient.get<Chat[]>('api/apps/chat/chats').pipe(
tap((response: Chat[]) => {
this._chats.next(response);
})
);
}
/**
* Get contact
*
* @param id
*/
getContact(id: string): Observable<any>
{
return this._httpClient.get<Contact>('api/apps/chat/contacts', {params: {id}}).pipe(
tap((response: Contact) => {
this._contact.next(response);
})
);
}
/**
* Get contacts
*/
getContacts(): Observable<any>
{
return this._httpClient.get<Contact[]>('api/apps/chat/contacts').pipe(
tap((response: Contact[]) => {
this._contacts.next(response);
})
);
}
/**
* Get profile
*/
getProfile(): Observable<any>
{
return this._httpClient.get<Profile>('api/apps/chat/profile').pipe(
tap((response: Profile) => {
this._profile.next(response);
})
);
}
/**
* Get chat
*
* @param id
*/
getChatById(id: string): Observable<any>
{
return this._httpClient.get<Chat>('api/apps/chat/chat', {params: {id}}).pipe(
map((chat) => {
// Update the chat
this._chat.next(chat);
// Return the chat
return chat;
}),
switchMap((chat) => {
if ( !chat )
{
return throwError('Could not found chat with id of ' + id + '!');
}
return of(chat);
})
);
}
/**
* Update chat
*
* @param id
* @param chat
*/
updateChat(id: string, chat: Chat): Observable<Chat>
{
return this.chats$.pipe(
take(1),
switchMap(chats => this._httpClient.patch<Chat>('api/apps/chat/chat', {
id,
chat
}).pipe(
map((updatedChat) => {
// Find the index of the updated chat
const index = chats.findIndex(item => item.id === id);
// Update the chat
chats[index] = updatedChat;
// Update the chats
this._chats.next(chats);
// Return the updated contact
return updatedChat;
}),
switchMap(updatedChat => this.chat$.pipe(
take(1),
filter(item => item && item.id === id),
tap(() => {
// Update the chat if it's selected
this._chat.next(updatedChat);
// Return the updated chat
return updatedChat;
})
))
))
);
}
/**
* Reset the selected chat
*/
resetChat(): void
{
this._chat.next(null);
}
}

View File

@@ -0,0 +1,55 @@
export interface Profile
{
id?: string;
name?: string;
email?: string;
avatar?: string;
about?: string;
}
export interface Contact
{
id?: string;
avatar?: string;
name?: string;
about?: string;
details?: {
emails?: {
email?: string;
label?: string;
}[];
phoneNumbers?: {
country?: string;
number?: string;
label?: string;
}[];
title?: string;
company?: string;
birthday?: string;
address?: string;
};
attachments?: {
media?: any[]
docs?: any[]
links?: any[]
};
}
export interface Chat
{
id?: string;
contactId?: string;
contact?: Contact;
unreadCount?: number;
muted?: boolean;
lastMessage?: string;
lastMessageAt?: string;
messages?: {
id?: string;
chatId?: string;
contactId?: string;
isMine?: boolean;
value?: string;
createdAt?: string;
}[];
}

View File

@@ -0,0 +1,190 @@
<div class="relative flex flex-auto w-full bg-card dark:bg-transparent">
<mat-drawer-container
class="flex-auto h-full"
[hasBackdrop]="false">
<!-- Drawer -->
<mat-drawer
class="w-full sm:w-100 lg:border-r lg:shadow-none dark:bg-gray-900"
[autoFocus]="false"
[(opened)]="drawerOpened"
#drawer>
<!-- New chat -->
<ng-container *ngIf="drawerComponent === 'new-chat'">
<chat-new-chat [drawer]="drawer"></chat-new-chat>
</ng-container>
<!-- Profile -->
<ng-container *ngIf="drawerComponent === 'profile'">
<chat-profile [drawer]="drawer"></chat-profile>
</ng-container>
</mat-drawer>
<!-- Drawer content -->
<mat-drawer-content class="flex overflow-hidden">
<!-- Chats list -->
<ng-container *ngIf="chats && chats.length > 0; else noChats">
<div class="relative flex flex-auto flex-col w-full min-w-0 lg:min-w-100 lg:max-w-100 bg-card dark:bg-transparent">
<!-- Header -->
<div class="flex flex-col flex-0 py-4 px-8 border-b bg-gray-50 dark:bg-transparent">
<div class="flex items-center">
<div
class="flex items-center mr-1 cursor-pointer"
(click)="openProfile()">
<div class="w-10 h-10">
<ng-container *ngIf="profile.avatar">
<img
class="object-cover w-full h-full rounded-full object-cover"
[src]="profile.avatar"
alt="Profile avatar"/>
</ng-container>
<ng-container *ngIf="!profile.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-lg uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{profile.name.charAt(0)}}
</div>
</ng-container>
</div>
<div class="ml-4 font-medium truncate">{{profile.name}}</div>
</div>
<button
class="ml-auto"
mat-icon-button
(click)="openNewChat()">
<mat-icon [svgIcon]="'heroicons_outline:plus-circle'"></mat-icon>
</button>
<button
class="ml-1 -mr-4"
mat-icon-button
[matMenuTriggerFor]="chatsHeaderMenu">
<mat-icon [svgIcon]="'heroicons_outline:dots-vertical'"></mat-icon>
<mat-menu #chatsHeaderMenu>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:user-group'"></mat-icon>
New group
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:chat-alt-2'"></mat-icon>
Create a room
</button>
<button
mat-menu-item
(click)="openProfile()">
<mat-icon [svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
Profile
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:archive'"></mat-icon>
Archived
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:star'"></mat-icon>
Starred
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:cog'"></mat-icon>
Settings
</button>
</mat-menu>
</button>
</div>
<!-- Search -->
<div class="mt-4">
<mat-form-field
class="fuse-mat-no-subscript fuse-mat-rounded fuse-mat-dense w-full"
[floatLabel]="'always'">
<mat-icon
matPrefix
class="icon-size-5"
[svgIcon]="'heroicons_solid:search'"></mat-icon>
<input
matInput
[autocomplete]="'off'"
[placeholder]="'Search or start new chat'"
(input)="filterChats(searchField.value)"
#searchField>
</mat-form-field>
</div>
</div>
<!-- Chats -->
<div class="flex-auto overflow-y-auto">
<ng-container *ngIf="filteredChats.length > 0; else noChats">
<ng-container *ngFor="let chat of filteredChats; trackBy: trackByFn">
<div
class="z-20 flex items-center py-5 px-8 cursor-pointer border-b hover:bg-hover"
[ngClass]="{'bg-primary-50 dark:bg-hover': selectedChat && selectedChat.id === chat.id}"
[routerLink]="[chat.id]">
<div class="relative flex flex-0 items-center justify-center w-10 h-10">
<ng-container *ngIf="chat.unreadCount > 0">
<div
class="absolute bottom-0 right-0 flex-0 w-2 h-2 -ml-0.5 rounded-full ring-2 ring-bg-card dark:ring-gray-900 bg-primary dark:bg-primary-500 text-on-primary"
[class.ring-primary-50]="selectedChat && selectedChat.id === chat.id"></div>
</ng-container>
<ng-container *ngIf="chat.contact.avatar">
<img
class="w-full h-full rounded-full object-cover"
[src]="chat.contact.avatar"
alt="Contact avatar"/>
</ng-container>
<ng-container *ngIf="!chat.contact.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-lg uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{chat.contact.name.charAt(0)}}
</div>
</ng-container>
</div>
<div class="min-w-0 ml-4">
<div class="font-medium leading-5 truncate">{{chat.contact.name}}</div>
<div
class="leading-5 truncate text-secondary"
[class.text-primary]="chat.unreadCount > 0"
[class.dark:text-primary-500]="chat.unreadCount > 0">
{{chat.lastMessage}}
</div>
</div>
<div class="flex flex-col items-end self-start ml-auto pl-2">
<div class="text-sm leading-5 text-secondary">{{chat.lastMessageAt}}</div>
<ng-container *ngIf="chat.muted">
<mat-icon
class="icon-size-5 text-hint"
[svgIcon]="'heroicons_solid:volume-off'"></mat-icon>
</ng-container>
</div>
</div>
</ng-container>
</ng-container>
</div>
</div>
</ng-container>
<!-- No chats template -->
<ng-template #noChats>
<div class="flex flex-auto flex-col items-center justify-center h-full">
<mat-icon
class="icon-size-24"
[svgIcon]="'iconsmind:speach_bubble'"></mat-icon>
<div class="mt-4 text-2xl font-semibold tracking-tight text-secondary">No chats</div>
</div>
</ng-template>
<!-- Conversation -->
<ng-container *ngIf="chats && chats.length > 0">
<div
class="flex-auto border-l"
[ngClass]="{'z-20 absolute inset-0 lg:static lg:inset-auto flex': selectedChat && selectedChat.id,
'hidden lg:flex': !selectedChat || !selectedChat.id}">
<router-outlet></router-outlet>
</div>
</ng-container>
</mat-drawer-content>
</mat-drawer-container>
</div>

View File

@@ -0,0 +1,138 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Chat, Profile } from 'app/modules/admin/apps/chat/chat.types';
import { ChatService } from 'app/modules/admin/apps/chat/chat.service';
@Component({
selector : 'chat-chats',
templateUrl : './chats.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChatsComponent implements OnInit, OnDestroy
{
chats: Chat[];
drawerComponent: 'profile' | 'new-chat';
drawerOpened: boolean = false;
filteredChats: Chat[];
profile: Profile;
selectedChat: Chat;
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(
private _chatService: ChatService,
private _changeDetectorRef: ChangeDetectorRef
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Chats
this._chatService.chats$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((chats: Chat[]) => {
this.chats = this.filteredChats = chats;
// Mark for check
this._changeDetectorRef.markForCheck();
});
// Profile
this._chatService.profile$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((profile: Profile) => {
this.profile = profile;
// Mark for check
this._changeDetectorRef.markForCheck();
});
// Selected chat
this._chatService.chat$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((chat: Chat) => {
this.selectedChat = chat;
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Filter the chats
*
* @param query
*/
filterChats(query: string): void
{
// Reset the filter
if ( !query )
{
this.filteredChats = this.chats;
return;
}
this.filteredChats = this.chats.filter((chat) => chat.contact.name.toLowerCase().includes(query.toLowerCase()));
}
/**
* Open the new chat sidebar
*/
openNewChat(): void
{
this.drawerComponent = 'new-chat';
this.drawerOpened = true;
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Open the profile sidebar
*/
openProfile(): void
{
this.drawerComponent = 'profile';
this.drawerOpened = true;
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}

View File

@@ -0,0 +1,86 @@
<div class="flex flex-col flex-auto h-full bg-card dark:bg-default">
<!-- Header -->
<div class="flex flex-0 items-center h-18 px-4 border-b bg-gray-50 dark:bg-transparent">
<button
mat-icon-button
(click)="drawer.close()">
<mat-icon [svgIcon]="'heroicons_outline:x'"></mat-icon>
</button>
<div class="ml-2 text-lg font-medium">Contact info</div>
</div>
<div class="overflow-y-auto">
<!-- Contact avatar & info -->
<div class="flex flex-col items-center mt-8">
<div class="w-40 h-40 rounded-full">
<ng-container *ngIf="chat.contact.avatar">
<img
class="w-full h-full rounded-full object-cover"
[src]="chat.contact.avatar"
[alt]="'Contact avatar'">
</ng-container>
<ng-container *ngIf="!chat.contact.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-8xl font-semibold uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{chat.contact.name.charAt(0)}}
</div>
</ng-container>
</div>
<div class="mt-4 text-lg font-medium">{{chat.contact.name}}</div>
<div class="mt-0.5 text-md text-secondary">{{chat.contact.about}}</div>
</div>
<div class="py-10 px-7">
<!-- Media -->
<div class="text-lg font-medium">Media</div>
<div class="grid grid-cols-4 gap-1 mt-4">
<ng-container *ngFor="let media of chat.contact.attachments.media">
<img
class="h-20 rounded object-cover"
[src]="media"/>
</ng-container>
</div>
<!-- Details -->
<div class="mt-10 space-y-4">
<div class="text-lg font-medium mb-3">Details</div>
<ng-container *ngIf="chat.contact.details.emails.length">
<div>
<div class="font-medium text-secondary">Email</div>
<div class="">{{chat.contact.details.emails[0].email}}</div>
</div>
</ng-container>
<ng-container *ngIf="chat.contact.details.phoneNumbers.length">
<div>
<div class="font-medium text-secondary">Phone number</div>
<div class="">{{chat.contact.details.phoneNumbers[0].number}}</div>
</div>
</ng-container>
<ng-container *ngIf="chat.contact.details.title">
<div>
<div class="font-medium text-secondary">Title</div>
<div class="">{{chat.contact.details.title}}</div>
</div>
</ng-container>
<ng-container *ngIf="chat.contact.details.company">
<div>
<div class="font-medium text-secondary">Company</div>
<div class="">{{chat.contact.details.company}}</div>
</div>
</ng-container>
<ng-container *ngIf="chat.contact.details.birthday">
<div>
<div class="font-medium text-secondary">Birthday</div>
<div class="">{{chat.contact.details.birthday}}</div>
</div>
</ng-container>
<ng-container *ngIf="chat.contact.details.address">
<div>
<div class="font-medium text-secondary">Address</div>
<div class="">{{chat.contact.details.address}}</div>
</div>
</ng-container>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
import { MatDrawer } from '@angular/material/sidenav';
import { Chat, Contact } from 'app/modules/admin/apps/chat/chat.types';
@Component({
selector : 'chat-contact-info',
templateUrl : './contact-info.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContactInfoComponent
{
@Input() chat: Chat;
@Input() drawer: MatDrawer;
/**
* Constructor
*/
constructor()
{
}
}

View File

@@ -0,0 +1,215 @@
<div class="flex flex-col flex-auto overflow-y-auto lg:overflow-hidden bg-card dark:bg-default">
<ng-container *ngIf="chat; else selectChatOrStartNew">
<mat-drawer-container
class="flex-auto h-full"
[hasBackdrop]="false">
<!-- Drawer -->
<mat-drawer
class="w-full sm:w-100 lg:border-l lg:shadow-none dark:bg-gray-900"
[autoFocus]="false"
[mode]="drawerMode"
[position]="'end'"
[(opened)]="drawerOpened"
#drawer>
<!-- Contact info -->
<chat-contact-info
[drawer]="drawer"
[chat]="chat"></chat-contact-info>
</mat-drawer>
<!-- Drawer content -->
<mat-drawer-content class="flex flex-col overflow-hidden">
<!-- Header -->
<div class="flex flex-0 items-center h-18 px-4 md:px-6 border-b bg-gray-50 dark:bg-transparent">
<!-- Back button -->
<a
class="lg:hidden md:-ml-2"
mat-icon-button
[routerLink]="['./']"
(click)="resetChat()">
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
</a>
<!-- Contact info -->
<div
class="flex items-center ml-2 lg:ml-0 mr-2 cursor-pointer"
(click)="openContactInfo()">
<div class="relative flex flex-0 items-center justify-center w-10 h-10">
<ng-container *ngIf="chat.contact.avatar">
<img
class="w-full h-full rounded-full object-cover"
[src]="chat.contact.avatar"
alt="Contact avatar"/>
</ng-container>
<ng-container *ngIf="!chat.contact.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-lg uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{chat.contact.name.charAt(0)}}
</div>
</ng-container>
</div>
<div class="ml-4 text-lg font-medium leading-5 truncate">{{chat.contact.name}}</div>
</div>
<button
class="ml-auto"
mat-icon-button
[matMenuTriggerFor]="conversationHeaderMenu">
<mat-icon [svgIcon]="'heroicons_outline:dots-vertical'"></mat-icon>
<mat-menu #conversationHeaderMenu>
<button
mat-menu-item
(click)="openContactInfo()">
<mat-icon [svgIcon]="'heroicons_outline:user-circle'"></mat-icon>
Contact info
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
Select messages
</button>
<button
mat-menu-item
(click)="toggleMuteNotifications()">
<ng-container *ngIf="!chat.muted">
<mat-icon [svgIcon]="'heroicons_outline:volume-off'"></mat-icon>
Mute notifications
</ng-container>
<ng-container *ngIf="chat.muted">
<mat-icon [svgIcon]="'heroicons_outline:volume-up'"></mat-icon>
Unmute notifications
</ng-container>
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:backspace'"></mat-icon>
Clear messages
</button>
<button mat-menu-item>
<mat-icon [svgIcon]="'heroicons_outline:trash'"></mat-icon>
Delete chat
</button>
</mat-menu>
</button>
</div>
<!-- Conversation -->
<div class="flex overflow-y-auto flex-col-reverse">
<div class="flex flex-col flex-auto flex-shrink p-6 bg-card dark:bg-transparent">
<ng-container *ngFor="let message of chat.messages; let i = index; let first = first; let last = last; trackBy: trackByFn">
<!-- Start of the day -->
<ng-container *ngIf="first || (chat.messages[i - 1].createdAt | date:'d') !== (message.createdAt | date:'d')">
<div class="flex items-center justify-center my-3 -mx-6">
<div class="flex-auto border-b"></div>
<div class="flex-0 mx-4 text-sm font-medium leading-5 text-secondary">
{{message.createdAt | date: 'longDate'}}
</div>
<div class="flex-auto border-b"></div>
</div>
</ng-container>
<div
class="flex flex-col"
[ngClass]="{'items-end': message.isMine,
'items-start': !message.isMine,
'mt-0.5': i > 0 && chat.messages[i - 1].isMine === message.isMine,
'mt-3': i > 0 && chat.messages[i - 1].isMine !== message.isMine}">
<!-- Bubble -->
<div
class="relative max-w-3/4 px-3 py-2 rounded-lg"
[ngClass]="{'bg-blue-500 text-blue-50': message.isMine,
'bg-gray-500 text-gray-50': !message.isMine}">
<!-- Speech bubble tail -->
<ng-container *ngIf="last || chat.messages[i + 1].isMine !== message.isMine">
<div
class="absolute bottom-0 w-3 transform"
[ngClass]="{'text-blue-500 -right-1 -mr-px mb-px': message.isMine,
'text-gray-500 -left-1 -ml-px mb-px -scale-x-1': !message.isMine}">
<ng-container *ngTemplateOutlet="speechBubbleExtension"></ng-container>
</div>
</ng-container>
<!-- Message -->
<div
class="min-w-4 leading-5"
[innerHTML]="message.value">
</div>
</div>
<!-- Time -->
<ng-container
*ngIf="first
|| last
|| chat.messages[i + 1].isMine !== message.isMine
|| chat.messages[i + 1].createdAt !== message.createdAt">
<div
class="my-0.5 text-sm font-medium text-secondary"
[ngClass]="{'mr-3': message.isMine,
'ml-3': !message.isMine}">
{{message.createdAt | date:'HH:mm'}}
</div>
</ng-container>
</div>
</ng-container>
</div>
</div>
<!-- Message field -->
<div class="flex items-end p-4 border-t bg-gray-50 dark:bg-transparent">
<div class="flex items-center h-11 my-px">
<button mat-icon-button>
<mat-icon [svgIcon]="'heroicons_outline:emoji-happy'"></mat-icon>
</button>
<button
class="ml-0.5"
mat-icon-button>
<mat-icon [svgIcon]="'heroicons_outline:paper-clip'"></mat-icon>
</button>
</div>
<mat-form-field class="fuse-mat-dense fuse-mat-no-subscript fuse-mat-rounded fuse-mat-bold w-full ml-4">
<textarea
class="min-h-5 my-0 resize-none"
style="margin: 11px 0 !important; padding: 0 !important;"
[rows]="1"
matInput
#messageInput></textarea>
</mat-form-field>
<div class="flex items-center h-11 my-px ml-4">
<button
mat-icon-button>
<mat-icon
class="transform rotate-90"
[svgIcon]="'heroicons_outline:paper-airplane'"></mat-icon>
</button>
</div>
</div>
</mat-drawer-content>
</mat-drawer-container>
</ng-container>
<!-- Select chat or start new template -->
<ng-template #selectChatOrStartNew>
<div class="flex flex-col flex-auto items-center justify-center bg-gray-100 dark:bg-transparent">
<mat-icon
class="icon-size-24"
[svgIcon]="'iconsmind:speach_bubble'"></mat-icon>
<div class="mt-4 text-2xl font-semibold tracking-tight text-secondary">Select a conversation or start a new chat</div>
</div>
</ng-template>
<!-- Speech bubble tail SVG -->
<!-- @formatter:off -->
<ng-template #speechBubbleExtension>
<svg width="100%" height="100%" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M1.01522827,0.516204834 C-8.83532715,54.3062744 61.7609863,70.5215302 64.8009949,64.3061218 C68.8074951,54.8859711 30.1663208,52.9997559 37.5036011,0.516204834 L1.01522827,0.516204834 Z" fill="currentColor" fill-rule="nonzero"></path>
</g>
</svg>
</ng-template>
<!-- @formatter:on -->
</div>

View File

@@ -0,0 +1,168 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, NgZone, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { FuseMediaWatcherService } from '@fuse/services/media-watcher';
import { Chat } from 'app/modules/admin/apps/chat/chat.types';
import { ChatService } from 'app/modules/admin/apps/chat/chat.service';
@Component({
selector : 'chat-conversation',
templateUrl : './conversation.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ConversationComponent implements OnInit, OnDestroy
{
@ViewChild('messageInput') messageInput: ElementRef;
chat: Chat;
drawerMode: 'over' | 'side' = 'side';
drawerOpened: boolean = false;
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(
private _changeDetectorRef: ChangeDetectorRef,
private _chatService: ChatService,
private _fuseMediaWatcherService: FuseMediaWatcherService,
private _ngZone: NgZone
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Chat
this._chatService.chat$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((chat: Chat) => {
this.chat = chat;
// Mark for check
this._changeDetectorRef.markForCheck();
});
// Subscribe to media changes
this._fuseMediaWatcherService.onMediaChange$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe(({matchingAliases}) => {
// Set the drawerMode if the given breakpoint is active
if ( matchingAliases.includes('lg') )
{
this.drawerMode = 'side';
}
else
{
this.drawerMode = 'over';
}
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Open the contact info
*/
openContactInfo(): void
{
// Open the drawer
this.drawerOpened = true;
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Reset the chat
*/
resetChat(): void
{
this._chatService.resetChat();
// Close the contact info in case it's opened
this.drawerOpened = false;
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Toggle mute notifications
*/
toggleMuteNotifications(): void
{
// Toggle the muted
this.chat.muted = !this.chat.muted;
// Update the chat on the server
this._chatService.updateChat(this.chat.id, this.chat).subscribe();
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Resize on 'input' and 'ngModelChange' events
*
* @private
*/
@HostListener('input')
@HostListener('ngModelChange')
private _resizeMessageInput(): void
{
// This doesn't need to trigger Angular's change detection by itself
this._ngZone.runOutsideAngular(() => {
setTimeout(() => {
// Set the height to 'auto' so we can correctly read the scrollHeight
this.messageInput.nativeElement.style.height = 'auto';
// Detect the changes so the height is applied
this._changeDetectorRef.detectChanges();
// Get the scrollHeight and subtract the vertical padding
this.messageInput.nativeElement.style.height = `${this.messageInput.nativeElement.scrollHeight}px`;
// Detect the changes one more time to apply the final height
this._changeDetectorRef.detectChanges();
});
});
}
}

View File

@@ -0,0 +1,51 @@
<div class="flex flex-col flex-auto h-full overflow-hidden bg-card dark:bg-default">
<!-- Header -->
<div class="flex flex-0 items-center h-18 -mb-px px-6 bg-gray-50 dark:bg-transparent">
<button
mat-icon-button
(click)="drawer.close()">
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
</button>
<div class="ml-2 text-2xl font-semibold">New chat</div>
</div>
<div class="relative overflow-y-auto">
<ng-container *ngIf="contacts.length; else noContacts">
<ng-container *ngFor="let contact of contacts; let i = index; trackBy: trackByFn">
<!-- Group -->
<ng-container *ngIf="i === 0 || contact.name.charAt(0) !== contacts[i - 1].name.charAt(0)">
<div class="z-10 sticky top-0 -mt-px px-6 py-1 md:px-8 border-t border-b font-medium uppercase text-secondary bg-gray-100 dark:bg-gray-900">
{{contact.name.charAt(0)}}
</div>
</ng-container>
<!-- Contact -->
<div class="z-20 flex items-center px-6 py-4 md:px-8 cursor-pointer hover:bg-hover border-b">
<div class="flex flex-0 items-center justify-center w-10 h-10 rounded-full overflow-hidden">
<ng-container *ngIf="contact.avatar">
<img
class="object-cover w-full h-full"
[src]="contact.avatar"
alt="Contact avatar"/>
</ng-container>
<ng-container *ngIf="!contact.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-lg uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{contact.name.charAt(0)}}
</div>
</ng-container>
</div>
<div class="min-w-0 ml-4">
<div class="font-medium leading-5 truncate">{{contact.name}}</div>
<div class="leading-5 truncate text-secondary">{{contact.about}}</div>
</div>
</div>
</ng-container>
</ng-container>
</div>
<!-- No contacts -->
<ng-template #noContacts>
<div class="p-8 sm:p-16 border-t text-4xl font-semibold tracking-tight text-center">There are no contacts!</div>
</ng-template>
</div>

View File

@@ -0,0 +1,68 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { MatDrawer } from '@angular/material/sidenav';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Contact } from 'app/modules/admin/apps/chat/chat.types';
import { ChatService } from 'app/modules/admin/apps/chat/chat.service';
@Component({
selector : 'chat-new-chat',
templateUrl : './new-chat.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NewChatComponent implements OnInit, OnDestroy
{
@Input() drawer: MatDrawer;
contacts: Contact[] = [];
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(private _chatService: ChatService)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Contacts
this._chatService.contacts$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((contacts: Contact[]) => {
this.contacts = contacts;
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}

View File

@@ -0,0 +1,80 @@
<div class="flex flex-col flex-auto overflow-y-auto bg-card dark:bg-default">
<!-- Header -->
<div class="flex flex-0 items-center h-18 px-6 border-b bg-gray-50 dark:bg-transparent">
<button
mat-icon-button
(click)="drawer.close()">
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
</button>
<div class="ml-2 text-2xl font-semibold">Profile</div>
</div>
<div class="px-6">
<!-- Profile photo -->
<div class="group relative flex flex-0 mt-8 mx-auto w-40 h-40 rounded-full">
<div class="hidden group-hover:flex absolute inset-0 flex-col items-center justify-center backdrop-filter backdrop-blur bg-opacity-80 rounded-full cursor-pointer bg-gray-800">
<mat-icon
class="text-white"
[svgIcon]="'heroicons_outline:camera'"></mat-icon>
<div class="mt-2 mx-6 font-medium text-center text-white">Change Profile Photo</div>
</div>
<ng-container *ngIf="profile.avatar">
<img
class="w-full h-full rounded-full object-cover"
[src]="profile.avatar"
[alt]="'Profile avatar'">
</ng-container>
<ng-container *ngIf="!profile.avatar">
<div class="flex items-center justify-center w-full h-full rounded-full text-8xl font-semibold uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
{{profile.name.charAt(0)}}
</div>
</ng-container>
</div>
<!-- Profile info -->
<div class="flex flex-col mt-8 mx-2">
<mat-form-field>
<mat-label>Name</mat-label>
<mat-icon
class="icon-size-5"
matPrefix
[svgIcon]="'heroicons_solid:user-circle'"></mat-icon>
<input
matInput
[ngModel]="profile.name">
</mat-form-field>
<mat-form-field>
<mat-label>Email</mat-label>
<mat-icon
class="icon-size-5"
matPrefix
[svgIcon]="'heroicons_solid:mail'"></mat-icon>
<input
matInput
[ngModel]="profile.email">
</mat-form-field>
<mat-form-field>
<mat-label>About</mat-label>
<mat-icon
class="icon-size-5"
matPrefix
[svgIcon]="'heroicons_solid:identification'"></mat-icon>
<input
matInput
[ngModel]="profile.about">
</mat-form-field>
<div class="flex items-center justify-end mt-4">
<button
(click)="drawer.close()"
mat-button>Cancel
</button>
<button
class="ml-2"
mat-flat-button
[color]="'primary'">Save
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,53 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { MatDrawer } from '@angular/material/sidenav';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Profile } from 'app/modules/admin/apps/chat/chat.types';
import { ChatService } from 'app/modules/admin/apps/chat/chat.service';
@Component({
selector : 'chat-profile',
templateUrl : './profile.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileComponent implements OnInit, OnDestroy
{
@Input() drawer: MatDrawer;
profile: Profile;
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(private _chatService: ChatService)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Profile
this._chatService.profile$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((profile: Profile) => {
this.profile = profile;
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}

View File

@@ -336,9 +336,10 @@
(click)="toggleContactTag(tag)"
matRipple>
<mat-checkbox
class="flex items-center h-10 min-h-10"
class="flex items-center h-10 min-h-10 pointer-events-none"
[checked]="contact.tags.includes(tag.id)"
[color]="'primary'"
[checked]="contact.tags.includes(tag.id)">
[disableRipple]="true">
</mat-checkbox>
<div class="ml-1">{{tag.title}}</div>
</div>

View File

@@ -47,8 +47,7 @@
<!-- Folders -->
<div class="font-medium">Folders</div>
<div
class="grid gap-4 mt-4"
style="grid-template-columns: repeat(auto-fill,minmax(160px,1fr))">
class="flex flex-wrap -m-2 mt-2">
<ng-container *ngFor="let folder of items.folders; trackBy:trackByFn">
<ng-container *ngTemplateOutlet="item, context: {$implicit: folder}"></ng-container>
</ng-container>
@@ -57,8 +56,7 @@
<!-- Files -->
<div class="font-medium mt-8">Files</div>
<div
class="grid gap-4 mt-4"
style="grid-template-columns: repeat(auto-fill,minmax(160px,1fr))">
class="flex flex-wrap -m-2 mt-2">
<ng-container *ngFor="let file of items.files; trackBy:trackByFn">
<ng-container *ngTemplateOutlet="item, context: {$implicit: file}"></ng-container>
</ng-container>
@@ -71,7 +69,7 @@
#item
let-item>
<div
class="flex flex-col shadow rounded-2xl cursor-pointer bg-card"
class="flex flex-col w-40 h-40 m-2 p-4 shadow rounded-2xl cursor-pointer bg-card"
(click)="goToItem(item.id)">
<div class="aspect-w-9 aspect-h-6">
<div class="flex items-center justify-center">
@@ -103,12 +101,12 @@
</ng-container>
</div>
</div>
<div
class="pb-4 px-4 text-center text-sm font-medium"
[matTooltip]="item.name">
<div class="truncate">{{item.name}}</div>
<div class="flex flex-col flex-auto justify-center text-center text-sm font-medium">
<div
class="truncate"
[matTooltip]="item.name">{{item.name}}</div>
<ng-container *ngIf="item.contents">
<div class="mt-0.5 text-secondary truncate">{{item.contents}}</div>
<div class="text-secondary truncate">{{item.contents}}</div>
</ng-container>
</div>
</div>

View File

@@ -1,11 +1,9 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDrawer } from '@angular/material/sidenav';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { FuseMediaWatcherService } from '@fuse/services/media-watcher';
import { FuseNavigationService } from '@fuse/components/navigation';
import { FileManagerService } from 'app/modules/admin/apps/file-manager/file-manager.service';
import { Item, Items } from 'app/modules/admin/apps/file-manager/file-manager.types';
@@ -29,11 +27,9 @@ export class FileManagerListComponent implements OnInit, OnDestroy
constructor(
private _activatedRoute: ActivatedRoute,
private _changeDetectorRef: ChangeDetectorRef,
@Inject(DOCUMENT) private _document: any,
private _router: Router,
private _fileManagerService: FileManagerService,
private _fuseMediaWatcherService: FuseMediaWatcherService,
private _fuseNavigationService: FuseNavigationService
private _fuseMediaWatcherService: FuseMediaWatcherService
)
{
}

View File

@@ -132,7 +132,7 @@
fuseScrollReset>
<!-- Thread -->
<div class="flex flex-col flex-0 w-full border rounded-2xl overflow-hidden bg-card dark:bg-black dark:bg-opacity-10">
<div class="flex flex-col flex-0 w-full shadow rounded-2xl overflow-hidden bg-card dark:bg-black dark:bg-opacity-10">
<div class="flex flex-col py-8 px-6">

View File

@@ -1,7 +1,6 @@
import { Component, ElementRef, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import * as moment from 'moment';
import { MailboxService } from 'app/modules/admin/apps/mailbox/mailbox.service';
import { MailboxComponent } from 'app/modules/admin/apps/mailbox/mailbox.component';
import { Mail, MailCategory } from 'app/modules/admin/apps/mailbox/mailbox.types';
@@ -119,77 +118,6 @@ export class MailboxListComponent implements OnInit, OnDestroy
this._mailboxService.selectedMailChanged.next(mail);
}
/**
* Generate and return mail list group label if necessary or return false
*
* @param index
*/
mailListGroupLabel(index: number): string | false
{
const previousMail = this.mails[index - 1];
const currentMail = this.mails[index];
// Generate and return label, if there is no previous mail
if ( !previousMail )
{
return this._generateMailListGroupLabel(this.mails[index].date);
}
// Return false, if the two dates are equal by day
if ( moment(previousMail.date, moment.ISO_8601).isSame(moment(currentMail.date, moment.ISO_8601), 'day') )
{
return false;
}
// Generate and return label
return this._generateMailListGroupLabel(this.mails[index].date);
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Generate a mail list group label based on the date
*
* @param mailDate
* @private
*/
private _generateMailListGroupLabel(mailDate: string): string
{
const date = moment(mailDate, moment.ISO_8601);
const today = moment();
const yesterday = moment().subtract(1, 'day');
// Check if the mail date is today
if ( date.isSame(today, 'day') )
{
// Return 'Today'
return 'Today';
}
// Check if the mail date is yesterday
if ( date.isSame(yesterday, 'day') )
{
// Return 'Yesterday'
return 'Yesterday';
}
// Check if we are in the same year with the mail date...
if ( date.isSame(today, 'year') )
{
// Return a date without a year
return date.format('MMMM DD');
}
// Return a date
return date.format('LL');
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Track by function for ngFor loops
*

View File

@@ -1,6 +1,6 @@
<div class="flex flex-col flex-auto w-full">
<!-- Title -->
<div class="mt-10 mb-8 mx-6 text-5xl font-extrabold tracking-tight leading-none">Mailbox</div>
<div class="mt-10 mb-8 mx-6 text-4xl font-extrabold tracking-tight leading-none">Mailbox</div>
<!-- Compose button -->
<button

View File

@@ -0,0 +1,176 @@
<div class="flex flex-col flex-auto md:w-160 md:min-w-160 -m-6">
<ng-container *ngIf="(note$ | async) as note">
<!-- Image -->
<ng-container *ngIf="note.image">
<div class="relative w-full">
<div class="absolute right-0 bottom-0 p-4">
<button
mat-icon-button
(click)="removeImage(note)">
<mat-icon
class="text-white"
[svgIcon]="'heroicons_outline:trash'"></mat-icon>
</button>
</div>
<img
class="w-full object-cover"
[src]="note.image">
</div>
</ng-container>
<div class="m-4">
<!-- Title -->
<div>
<input
class="w-full p-2 text-2xl"
[placeholder]="'Title'"
[(ngModel)]="note.title"
(input)="updateNoteDetails(note)">
</div>
<!-- Note -->
<div>
<textarea
class="w-full my-2.5 p-2"
fuseAutogrow
[placeholder]="'Note'"
[(ngModel)]="note.content"
(input)="updateNoteDetails(note)"></textarea>
</div>
<!-- Tasks -->
<ng-container *ngIf="note.tasks">
<div class="mx-2 mt-4 space-y-1.5">
<ng-container *ngFor="let task of note.tasks; trackBy: trackByFn">
<div class="group flex items-center">
<mat-checkbox
class="flex items-center"
[color]="'primary'"
[(ngModel)]="task.completed"
(change)="updateTaskOnNote(note, task)"></mat-checkbox>
<input
class="w-full px-1 py-0.5"
[ngClass]="{'text-secondary line-through': task.completed}"
[placeholder]="'Task'"
[(ngModel)]="task.content"
(input)="updateTaskOnNote(note, task)">
<mat-icon
class="hidden group-hover:flex ml-auto icon-size-5 cursor-pointer"
[svgIcon]="'heroicons_solid:x'"
(click)="removeTaskFromNote(note, task)"></mat-icon>
</div>
</ng-container>
<div class="flex items-center">
<mat-icon
class="-ml-0.5 icon-size-5 text-hint"
[svgIcon]="'heroicons_solid:plus'"></mat-icon>
<input
class="w-full ml-1.5 px-1 py-0.5"
[placeholder]="'Add task'"
(keydown.enter)="addTaskToNote(note, newTaskInput.value); newTaskInput.value = ''"
#newTaskInput>
</div>
</div>
</ng-container>
<!-- Labels -->
<ng-container *ngIf="note.labels && note.labels.length">
<div class="flex flex-wrap items-center mx-1 mt-6">
<ng-container *ngFor="let label of note.labels; trackBy: trackByFn">
<div class="flex items-center m-1 py-0.5 px-3 rounded-full text-sm font-medium text-gray-500 bg-gray-100 dark:text-gray-300 dark:bg-gray-700">
<div>
{{label.title}}
</div>
<mat-icon
class="ml-1 icon-size-4 cursor-pointer"
[svgIcon]="'heroicons_solid:x-circle'"
(click)="toggleLabelOnNote(note, label)"></mat-icon>
</div>
</ng-container>
</div>
</ng-container>
<!-- Add Actions -->
<ng-container *ngIf="!note.id">
<div class="flex items-center justify-end mt-4">
<!-- Save -->
<button
mat-flat-button
[color]="'primary'"
[disabled]="!note.title && !note.content"
(click)="createNote(note)">
Save
</button>
</div>
</ng-container>
<!-- Edit Actions -->
<ng-container *ngIf="note.id">
<div class="flex items-center justify-between mt-4">
<div class="flex items-center space-x-2">
<!-- Image -->
<div>
<input
id="image-file-input"
class="absolute h-0 w-0 opacity-0 invisible pointer-events-none"
type="file"
[multiple]="false"
[accept]="'image/jpeg, image/png'"
(change)="uploadImage(note, imageFileInput.files)"
#imageFileInput>
<label
class="flex items-center justify-center w-10 h-10 rounded-full cursor-pointer hover:bg-gray-400 hover:bg-opacity-20 dark:hover:bg-black dark:hover:bg-opacity-5"
for="image-file-input"
matRipple>
<mat-icon [svgIcon]="'heroicons_outline:photograph'"></mat-icon>
</label>
</div>
<!-- Checklist -->
<button
mat-icon-button
(click)="addTasksToNote(note)">
<mat-icon [svgIcon]="'heroicons_outline:clipboard-list'"></mat-icon>
</button>
<!-- Labels -->
<button
mat-icon-button
[matMenuTriggerFor]="labelsMenu">
<mat-icon [svgIcon]="'heroicons_outline:tag'"></mat-icon>
</button>
<mat-menu #labelsMenu="matMenu">
<ng-container *ngIf="(labels$ | async) as labels">
<ng-container *ngFor="let label of labels">
<button
mat-menu-item
(click)="toggleLabelOnNote(note, label)">
<span class="flex items-center">
<mat-checkbox
class="flex items-center pointer-events-none"
[color]="'primary'"
[checked]="isNoteHasLabel(note, label)"
disableRipple></mat-checkbox>
<span class="ml-1 leading-5">{{label.title}}</span>
</span>
</button>
</ng-container>
</ng-container>
</mat-menu>
<!-- Archive -->
<button
mat-icon-button
(click)="toggleArchiveOnNote(note)">
<mat-icon [svgIcon]="'heroicons_outline:archive'"></mat-icon>
</button>
<!-- Delete -->
<button
mat-icon-button
(click)="deleteNote(note)">
<mat-icon [svgIcon]="'heroicons_outline:trash'"></mat-icon>
</button>
</div>
<!-- Close -->
<button
mat-flat-button
matDialogClose>
Close
</button>
</div>
</ng-container>
</div>
</ng-container>
</div>

View File

@@ -0,0 +1,346 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { debounceTime, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { Observable, of, Subject } from 'rxjs';
import { NotesService } from 'app/modules/admin/apps/notes/notes.service';
import { Label, Note, Task } from 'app/modules/admin/apps/notes/notes.types';
@Component({
selector : 'notes-details',
templateUrl : './details.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotesDetailsComponent implements OnInit, OnDestroy
{
note$: Observable<Note>;
labels$: Observable<Label[]>;
noteChanged: Subject<Note> = new Subject<Note>();
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(
private _changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) private _data: { note: Note },
private _notesService: NotesService,
private _matDialogRef: MatDialogRef<NotesDetailsComponent>
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Edit
if ( this._data.note.id )
{
// Request the data from the server
this._notesService.getNoteById(this._data.note.id).subscribe();
// Get the note
this.note$ = this._notesService.note$;
}
// Add
else
{
// Create an empty note
const note = {
id : null,
title : '',
content : '',
tasks : null,
image : null,
reminder : null,
labels : [],
archived : false,
createdAt: null,
updatedAt: null
};
this.note$ = of(note);
}
// Get the labels
this.labels$ = this._notesService.labels$;
// Subscribe to note updates
this.noteChanged
.pipe(
takeUntil(this._unsubscribeAll),
debounceTime(500),
switchMap((note) => this._notesService.updateNote(note)))
.subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Create a new note
*
* @param note
*/
createNote(note: Note): void
{
this._notesService.createNote(note).pipe(
map(() => {
// Get the note
this.note$ = this._notesService.note$;
})).subscribe();
}
/**
* Upload image to given note
*
* @param note
* @param fileList
*/
uploadImage(note: Note, fileList: FileList): void
{
// Return if canceled
if ( !fileList.length )
{
return;
}
const allowedTypes = ['image/jpeg', 'image/png'];
const file = fileList[0];
// Return if the file is not allowed
if ( !allowedTypes.includes(file.type) )
{
return;
}
this._readAsDataURL(file).then((data) => {
// Update the image
note.image = data;
// Update the note
this.noteChanged.next(note);
});
}
/**
* Remove the image on the given note
*
* @param note
*/
removeImage(note: Note): void
{
note.image = null;
// Update the note
this.noteChanged.next(note);
}
/**
* Add an empty tasks array to note
*
* @param note
*/
addTasksToNote(note): void
{
if ( !note.tasks )
{
note.tasks = [];
}
}
/**
* Add task to the given note
*
* @param note
* @param task
*/
addTaskToNote(note: Note, task: string): void
{
if ( task.trim() === '' )
{
return;
}
// Add the task
this._notesService.addTask(note, task).subscribe();
}
/**
* Remove the given task from given note
*
* @param note
* @param task
*/
removeTaskFromNote(note: Note, task: Task): void
{
// Remove the task
note.tasks = note.tasks.filter((item) => item.id !== task.id);
// Update the note
this.noteChanged.next(note);
}
/**
* Update the given task on the given note
*
* @param note
* @param task
*/
updateTaskOnNote(note: Note, task: Task): void
{
// If the task is already available on the item
if ( task.id )
{
// Update the note
this.noteChanged.next(note);
}
}
/**
* Is the given note has the given label
*
* @param note
* @param label
*/
isNoteHasLabel(note: Note, label: Label): boolean
{
return !!note.labels.find((item) => item.id === label.id);
}
/**
* Toggle the given label on the given note
*
* @param note
* @param label
*/
toggleLabelOnNote(note: Note, label: Label): void
{
// If the note already has the label
if ( this.isNoteHasLabel(note, label) )
{
note.labels = note.labels.filter((item) => item.id !== label.id);
}
// Otherwise
else
{
note.labels.push(label);
}
// Update the note
this.noteChanged.next(note);
}
/**
* Toggle archived status on the given note
*
* @param note
*/
toggleArchiveOnNote(note: Note): void
{
note.archived = !note.archived;
// Update the note
this.noteChanged.next(note);
// Close the dialog
this._matDialogRef.close();
}
/**
* Update the note details
*
* @param note
*/
updateNoteDetails(note: Note): void
{
this.noteChanged.next(note);
}
/**
* Delete the given note
*
* @param note
*/
deleteNote(note: Note): void
{
this._notesService.deleteNote(note)
.subscribe((isDeleted) => {
// Return if the note wasn't deleted...
if ( !isDeleted )
{
return;
}
// Close the dialog
this._matDialogRef.close();
});
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Read the given file for demonstration purposes
*
* @param file
*/
private _readAsDataURL(file: File): Promise<any>
{
// Return a new promise
return new Promise((resolve, reject) => {
// Create a new reader
const reader = new FileReader();
// Resolve the promise on success
reader.onload = () => {
resolve(reader.result);
};
// Reject the promise on error
reader.onerror = (e) => {
reject(e);
};
// Read the file as the
reader.readAsDataURL(file);
});
}
}

View File

@@ -0,0 +1,54 @@
<div class="flex flex-col flex-auto w-80 min-w-80 p-2 md:p-4">
<!-- Header -->
<div class="flex items-center justify-between">
<div class="text-2xl font-semibold">Edit labels</div>
<button
matDialogClose
mat-icon-button>
<mat-icon [svgIcon]="'heroicons_outline:x'"></mat-icon>
</button>
</div>
<!-- New label -->
<mat-form-field
class="fuse-mat-dense w-full mt-8"
[floatLabel]="'always'">
<input
name="new-label"
[autocomplete]="'off'"
[placeholder]="'Create new label'"
matInput
#newLabelInput>
<button
[class.invisible]="newLabelInput.value.trim() === ''"
mat-icon-button
(click)="addLabel(newLabelInput.value); newLabelInput.value = ''"
matSuffix>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:check-circle'"></mat-icon>
</button>
</mat-form-field>
<!-- Labels -->
<div class="flex flex-col mt-4">
<ng-container *ngIf="(labels$ | async) as labels">
<ng-container *ngFor="let label of labels; trackBy: trackByFn">
<mat-form-field class="fuse-mat-dense w-full">
<button
mat-icon-button
matPrefix
(click)="deleteLabel(label.id)">
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:trash'"></mat-icon>
</button>
<input
[autocomplete]="'off'"
[(ngModel)]="label.title"
(input)="updateLabel(label)"
required
matInput>
</mat-form-field>
</ng-container>
</ng-container>
</div>
</div>

View File

@@ -0,0 +1,112 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { NotesService } from 'app/modules/admin/apps/notes/notes.service';
import { Label } from 'app/modules/admin/apps/notes/notes.types';
import { debounceTime, filter, switchMap, takeUntil } from 'rxjs/operators';
import { Observable, Subject } from 'rxjs';
@Component({
selector : 'notes-labels',
templateUrl : './labels.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotesLabelsComponent implements OnInit, OnDestroy
{
labels$: Observable<Label[]>;
labelChanged: Subject<Label> = new Subject<Label>();
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(
private _changeDetectorRef: ChangeDetectorRef,
private _notesService: NotesService
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the labels
this.labels$ = this._notesService.labels$;
// Subscribe to label updates
this.labelChanged
.pipe(
takeUntil(this._unsubscribeAll),
debounceTime(500),
filter((label) => label.title.trim() !== ''),
switchMap((label) => this._notesService.updateLabel(label)))
.subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Add label
*
* @param title
*/
addLabel(title: string): void
{
this._notesService.addLabel(title).subscribe();
}
/**
* Update label
*/
updateLabel(label: Label): void
{
this.labelChanged.next(label);
}
/**
* Delete label
*
* @param id
*/
deleteLabel(id: string): void
{
this._notesService.deleteLabel(id).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}

View File

@@ -0,0 +1,221 @@
<div class="absolute inset-0 flex flex-col min-w-0 overflow-hidden">
<mat-drawer-container class="flex-auto h-full bg-card dark:bg-transparent">
<!-- Drawer -->
<mat-drawer
class="w-2/3 sm:w-72 lg:w-56 border-r-0 bg-default"
[mode]="drawerMode"
[opened]="drawerOpened"
#drawer>
<div class="p-6 lg:py-8 lg:pl-4 lg:pr-0">
<!-- Filters -->
<div class="space-y-2">
<!-- Notes -->
<div
class="relative flex items-center py-2 px-4 font-medium rounded-full cursor-pointer"
[ngClass]="{'bg-gray-200 dark:bg-gray-700 text-primary dark:text-primary-400': filterStatus === 'notes',
'text-hint hover:bg-hover': filterStatus !== 'notes'}"
(click)="resetFilter()"
matRipple
[matRippleDisabled]="filterStatus === 'notes'">
<mat-icon
class="text-current"
[svgIcon]="'heroicons_outline:pencil-alt'"></mat-icon>
<div class="ml-3 leading-5 select-none text-default">Notes</div>
</div>
<!-- Archive -->
<div
class="relative flex items-center py-2 px-4 font-medium rounded-full cursor-pointer"
[ngClass]="{'bg-gray-200 dark:bg-gray-700 text-primary dark:text-primary-400': filterStatus === 'archived',
'text-hint hover:bg-hover': filterStatus !== 'archived'}"
(click)="filterByArchived()"
matRipple
[matRippleDisabled]="filterStatus === 'archived'">
<mat-icon
class="text-current"
[svgIcon]="'heroicons_outline:archive'"></mat-icon>
<div class="ml-3 leading-5 select-none text-default">Archive</div>
</div>
<!-- Labels -->
<ng-container *ngIf="(labels$ | async) as labels">
<ng-container *ngFor="let label of labels; trackBy: trackByFn">
<div
class="relative flex items-center py-2 px-4 font-medium rounded-full cursor-pointer"
[ngClass]="{'bg-gray-200 dark:bg-gray-700 text-primary dark:text-primary-400': 'label:' + label.id === filterStatus,
'text-hint hover:bg-hover': 'label:' + label.id !== filterStatus}"
(click)="filterByLabel(label.id)"
matRipple
[matRippleDisabled]="'label:' + label.id === filterStatus">
<mat-icon
class="text-current"
[svgIcon]="'heroicons_outline:tag'"></mat-icon>
<div class="ml-3 leading-5 select-none text-default">{{label.title}}</div>
</div>
</ng-container>
</ng-container>
<!-- Edit Labels -->
<div
class="relative flex items-center py-2 px-4 font-medium rounded-full cursor-pointer hover:bg-hover"
(click)="openEditLabelsDialog()"
matRipple>
<mat-icon
class="text-hint"
[svgIcon]="'heroicons_outline:pencil'"></mat-icon>
<div class="ml-3 leading-5 select-none">Edit labels</div>
</div>
</div>
</div>
</mat-drawer>
<mat-drawer-content class="flex flex-col bg-gray-100 dark:bg-transparent">
<!-- Main -->
<div class="flex flex-col flex-auto p-6 md:p-8">
<!-- Header -->
<div class="flex items-center">
<div class="flex items-center flex-auto">
<button
class="flex lg:hidden -ml-2"
mat-icon-button
(click)="drawer.toggle()">
<mat-icon [svgIcon]="'heroicons_outline:menu'"></mat-icon>
</button>
<mat-form-field class="fuse-mat-rounded fuse-mat-dense fuse-mat-no-subscript flex-auto ml-4 lg:ml-0">
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:search'"
matPrefix></mat-icon>
<input
matInput
[autocomplete]="'off'"
[placeholder]="'Search notes'"
(input)="filterByQuery(searchInput.value)"
#searchInput>
</mat-form-field>
</div>
<!-- New note -->
<button
class="ml-4 px-1 sm:px-4 min-w-10"
mat-flat-button
[color]="'primary'"
(click)="addNewNote()">
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:plus-circle'"></mat-icon>
<span class="hidden sm:inline-block ml-2">New note</span>
</button>
</div>
<!-- Notes -->
<ng-container *ngIf="(notes$ | async) as notes; else loading">
<ng-container *ngIf="notes.length; else noNotes">
<!-- Masonry layout -->
<fuse-masonry
class="-mx-2 mt-8"
[items]="notes"
[columns]="masonryColumns"
[columnsTemplate]="columnsTemplate">
<!-- Columns template -->
<ng-template
#columnsTemplate
let-columns>
<!-- Columns -->
<ng-container *ngFor="let column of columns; trackBy: trackByFn">
<!-- Column -->
<div class="flex-1 px-2 space-y-4">
<ng-container *ngFor="let note of column.items; trackBy: trackByFn">
<!-- Note -->
<div
class="flex flex-col shadow rounded-2xl overflow-hidden cursor-pointer bg-card"
(click)="openNoteDialog(note)">
<!-- Image -->
<ng-container *ngIf="note.image">
<img
class="w-full object-cover"
[src]="note.image">
</ng-container>
<div class="flex flex-auto flex-col p-6 space-y-4">
<!-- Title -->
<ng-container *ngIf="note.title">
<div class="font-semibold line-clamp-3">
{{note.title}}
</div>
</ng-container>
<!-- Content -->
<ng-container *ngIf="note.content">
<div [class.text-xl]="note.content.length < 70">
{{note.content}}
</div>
</ng-container>
<!-- Tasks -->
<ng-container *ngIf="note.tasks">
<div class="space-y-1.5">
<ng-container *ngFor="let task of note.tasks; trackBy: trackByFn">
<div class="flex items-center">
<ng-container *ngIf="!task.completed">
<div class="flex items-center justify-center w-5 h-5">
<div class="w-4 h-4 rounded-full border-2"></div>
</div>
</ng-container>
<ng-container *ngIf="task.completed">
<mat-icon
class="text-hint icon-size-5"
[svgIcon]="'heroicons_solid:check-circle'"></mat-icon>
</ng-container>
<div
class="ml-1.5 leading-5"
[ngClass]="{'text-secondary line-through': task.completed}">
{{task.content}}
</div>
</div>
</ng-container>
</div>
</ng-container>
<!-- Labels -->
<ng-container *ngIf="note.labels">
<div class="flex flex-wrap items-center -m-1">
<ng-container *ngFor="let label of note.labels; trackBy: trackByFn">
<div class="m-1 py-0.5 px-3 rounded-full text-sm font-medium text-gray-500 bg-gray-100 dark:text-gray-300 dark:bg-gray-700">
{{label.title}}
</div>
</ng-container>
</div>
</ng-container>
</div>
</div>
</ng-container>
</div>
</ng-container>
</ng-template>
</fuse-masonry>
</ng-container>
</ng-container>
<!-- Loading template -->
<ng-template #loading>
<div class="flex flex-auto flex-col items-center justify-center bg-gray-100 dark:bg-transparent">
<div class="mt-4 text-2xl font-semibold tracking-tight text-secondary">Loading...</div>
</div>
</ng-template>
<!-- No notes template -->
<ng-template #noNotes>
<div class="flex flex-auto flex-col items-center justify-center bg-gray-100 dark:bg-transparent">
<mat-icon
class="icon-size-24"
[svgIcon]="'iconsmind:file_hide'"></mat-icon>
<div class="mt-4 text-2xl font-semibold tracking-tight text-secondary">There are no notes!</div>
</div>
</ng-template>
</div>
</mat-drawer-content>
</mat-drawer-container>
</div>

View File

@@ -0,0 +1,255 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
import { FuseMediaWatcherService } from '@fuse/services/media-watcher';
import { NotesDetailsComponent } from 'app/modules/admin/apps/notes/details/details.component';
import { NotesLabelsComponent } from 'app/modules/admin/apps/notes/labels/labels.component';
import { NotesService } from 'app/modules/admin/apps/notes/notes.service';
import { Label, Note } from 'app/modules/admin/apps/notes/notes.types';
import { cloneDeep } from 'lodash-es';
@Component({
selector : 'notes-list',
templateUrl : './list.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotesListComponent implements OnInit, OnDestroy
{
labels$: Observable<Label[]>;
notes$: Observable<Note[]>;
drawerMode: 'over' | 'side' = 'side';
drawerOpened: boolean = true;
filter$: BehaviorSubject<string> = new BehaviorSubject('notes');
searchQuery$: BehaviorSubject<string> = new BehaviorSubject(null);
masonryColumns: number = 4;
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(
private _changeDetectorRef: ChangeDetectorRef,
private _fuseMediaWatcherService: FuseMediaWatcherService,
private _matDialog: MatDialog,
private _notesService: NotesService
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Get the filter status
*/
get filterStatus(): string
{
return this.filter$.value;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Request the data from the server
this._notesService.getLabels().subscribe();
this._notesService.getNotes().subscribe();
// Get labels
this.labels$ = this._notesService.labels$;
// Get notes
this.notes$ = combineLatest([this._notesService.notes$, this.filter$, this.searchQuery$]).pipe(
distinctUntilChanged(),
map(([notes, filter, searchQuery]) => {
if ( !notes || !notes.length )
{
return;
}
// Store the filtered notes
let filteredNotes = notes;
// Filter by query
if ( searchQuery )
{
searchQuery = searchQuery.trim().toLowerCase();
filteredNotes = filteredNotes.filter((note) => note.title.toLowerCase().includes(searchQuery) || note.content.toLowerCase().includes(searchQuery));
}
// Show all
if ( filter === 'notes' )
{
// Do nothing
}
// Show archive
const isArchive = filter === 'archived';
filteredNotes = filteredNotes.filter((note) => note.archived === isArchive);
// Filter by label
if ( filter.startsWith('label:') )
{
const labelId = filter.split(':')[1];
filteredNotes = filteredNotes.filter((note) => !!note.labels.find((item) => item.id === labelId));
}
return filteredNotes;
})
);
// Subscribe to media changes
this._fuseMediaWatcherService.onMediaChange$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe(({matchingAliases}) => {
// Set the drawerMode and drawerOpened if the given breakpoint is active
if ( matchingAliases.includes('lg') )
{
this.drawerMode = 'side';
this.drawerOpened = true;
}
else
{
this.drawerMode = 'over';
this.drawerOpened = false;
}
// Set the masonry columns
//
// This if block structured in a way so that only the
// biggest matching alias will be used to set the column
// count.
if ( matchingAliases.includes('xl') )
{
this.masonryColumns = 5;
}
else if ( matchingAliases.includes('lg') )
{
this.masonryColumns = 4;
}
else if ( matchingAliases.includes('md') )
{
this.masonryColumns = 3;
}
else if ( matchingAliases.includes('sm') )
{
this.masonryColumns = 2;
}
else
{
this.masonryColumns = 1;
}
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Add a new note
*/
addNewNote(): void
{
this._matDialog.open(NotesDetailsComponent, {
autoFocus: false,
data : {
note: {}
}
});
}
/**
* Open the edit labels dialog
*/
openEditLabelsDialog(): void
{
this._matDialog.open(NotesLabelsComponent, {autoFocus: false});
}
/**
* Open the note dialog
*/
openNoteDialog(note: Note): void
{
this._matDialog.open(NotesDetailsComponent, {
autoFocus: false,
data : {
note: cloneDeep(note)
}
});
}
/**
* Filter by archived
*/
filterByArchived(): void
{
this.filter$.next('archived');
}
/**
* Filter by label
*
* @param labelId
*/
filterByLabel(labelId: string): void
{
const filterValue = `label:${labelId}`;
this.filter$.next(filterValue);
}
/**
* Filter by query
*
* @param query
*/
filterByQuery(query: string): void
{
this.searchQuery$.next(query);
}
/**
* Reset filter
*/
resetFilter(): void
{
this.filter$.next('notes');
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}

View File

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

View File

@@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
@Component({
selector : 'notes',
templateUrl : './notes.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotesComponent
{
/**
* Constructor
*/
constructor()
{
}
}

View File

@@ -0,0 +1,46 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatRippleModule } from '@angular/material/core';
import { MatSidenavModule } from '@angular/material/sidenav';
import { FuseAutogrowModule } from '@fuse/directives/autogrow';
import { FuseMasonryModule } from '@fuse/components/masonry/masonry.module';
import { SharedModule } from 'app/shared/shared.module';
import { NotesComponent } from 'app/modules/admin/apps/notes/notes.component';
import { NotesDetailsComponent } from 'app/modules/admin/apps/notes/details/details.component';
import { NotesListComponent } from 'app/modules/admin/apps/notes/list/list.component';
import { NotesLabelsComponent } from 'app/modules/admin/apps/notes/labels/labels.component';
import { notesRoutes } from 'app/modules/admin/apps/notes/notes.routing';
@NgModule({
declarations: [
NotesComponent,
NotesDetailsComponent,
NotesListComponent,
NotesLabelsComponent
],
imports : [
RouterModule.forChild(notesRoutes),
MatButtonModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatMenuModule,
MatRippleModule,
MatSidenavModule,
FuseAutogrowModule,
FuseMasonryModule,
SharedModule
]
})
export class NotesModule
{
}

View File

@@ -0,0 +1,16 @@
import { Route } from '@angular/router';
import { NotesComponent } from 'app/modules/admin/apps/notes/notes.component';
import { NotesListComponent } from 'app/modules/admin/apps/notes/list/list.component';
export const notesRoutes: Route[] = [
{
path : '',
component: NotesComponent,
children : [
{
path : '',
component: NotesListComponent
}
]
}
];

View File

@@ -0,0 +1,239 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, concat, Observable, of, throwError } from 'rxjs';
import { map, switchMap, take, tap } from 'rxjs/operators';
import { Label, Note, Task } from 'app/modules/admin/apps/notes/notes.types';
import { cloneDeep } from 'lodash-es';
@Injectable({
providedIn: 'root'
})
export class NotesService
{
// Private
private _labels: BehaviorSubject<Label[] | null> = new BehaviorSubject(null);
private _note: BehaviorSubject<Note | null> = new BehaviorSubject(null);
private _notes: BehaviorSubject<Note[] | null> = new BehaviorSubject(null);
/**
* Constructor
*/
constructor(private _httpClient: HttpClient)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Getter for labels
*/
get labels$(): Observable<Label[]>
{
return this._labels.asObservable();
}
/**
* Getter for notes
*/
get notes$(): Observable<Note[]>
{
return this._notes.asObservable();
}
/**
* Getter for note
*/
get note$(): Observable<Note>
{
return this._note.asObservable();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Get labels
*/
getLabels(): Observable<Label[]>
{
return this._httpClient.get<Label[]>('api/apps/notes/labels').pipe(
tap((response: Label[]) => {
this._labels.next(response);
})
);
}
/**
* Add label
*
* @param title
*/
addLabel(title: string): Observable<Label[]>
{
return this._httpClient.post<Label[]>('api/apps/notes/labels', {title}).pipe(
tap((labels) => {
// Update the labels
this._labels.next(labels);
})
);
}
/**
* Update label
*
* @param label
*/
updateLabel(label: Label): Observable<Label[]>
{
return this._httpClient.patch<Label[]>('api/apps/notes/labels', {label}).pipe(
tap((labels) => {
// Update the notes
this.getNotes().subscribe();
// Update the labels
this._labels.next(labels);
})
);
}
/**
* Delete a label
*
* @param id
*/
deleteLabel(id: string): Observable<Label[]>
{
return this._httpClient.delete<Label[]>('api/apps/notes/labels', {params: {id}}).pipe(
tap((labels) => {
// Update the notes
this.getNotes().subscribe();
// Update the labels
this._labels.next(labels);
})
);
}
/**
* Get notes
*/
getNotes(): Observable<Note[]>
{
return this._httpClient.get<Note[]>('api/apps/notes/all').pipe(
tap((response: Note[]) => {
this._notes.next(response);
})
);
}
/**
* Get note by id
*/
getNoteById(id: string): Observable<Note>
{
return this._notes.pipe(
take(1),
map((notes) => {
// Find within the folders and files
const note = notes.find(value => value.id === id) || null;
// Update the note
this._note.next(note);
// Return the note
return note;
}),
switchMap((note) => {
if ( !note )
{
return throwError('Could not found the note with id of ' + id + '!');
}
return of(note);
})
);
}
/**
* Add task to the given note
*
* @param note
* @param task
*/
addTask(note: Note, task: string): Observable<Note>
{
return this._httpClient.post<Note>('api/apps/notes/tasks', {
note,
task
}).pipe(switchMap(() => this.getNotes().pipe(
switchMap(() => this.getNoteById(note.id))
)));
}
/**
* Create note
*
* @param note
*/
createNote(note: Note): Observable<Note>
{
return this._httpClient.post<Note>('api/apps/notes', {note}).pipe(
switchMap((response) => this.getNotes().pipe(
switchMap(() => this.getNoteById(response.id).pipe(
map(() => response)
))
)));
}
/**
* Update the note
*
* @param note
*/
updateNote(note: Note): Observable<Note>
{
// Clone the note to prevent accidental reference based updates
const updatedNote = cloneDeep(note) as any;
// Before sending the note to the server, handle the labels
if ( updatedNote.labels.length )
{
updatedNote.labels = updatedNote.labels.map((label) => label.id);
}
return this._httpClient.patch<Note>('api/apps/notes', {updatedNote}).pipe(
tap((response) => {
// Update the notes
this.getNotes().subscribe();
})
);
}
/**
* Delete the note
*
* @param note
*/
deleteNote(note: Note): Observable<boolean>
{
return this._httpClient.delete<boolean>('api/apps/notes', {params: {id: note.id}}).pipe(
map((isDeleted: boolean) => {
// Update the notes
this.getNotes().subscribe();
// Return the deleted status
return isDeleted;
})
);
}
}

View File

@@ -0,0 +1,25 @@
export interface Task
{
id?: string;
content?: string;
completed?: string;
}
export interface Label
{
id?: string;
title?: string;
}
export interface Note
{
id?: string;
title?: string;
content?: string;
tasks?: Task[];
image?: string | null;
labels?: Label[];
archived?: boolean;
createdAt?: string;
updatedAt?: string | null;
}

View File

@@ -65,7 +65,7 @@
<!-- Task -->
<div
[id]="'task-' + task.id"
class="group w-full h-16 select-none hover:bg-hover"
class="group w-full h-16 select-none hover:bg-gray-50 dark:hover:bg-hover"
*ngFor="let task of tasks; trackBy: trackByFn"
[ngClass]="{'text-lg font-semibold bg-gray-100 dark:bg-card': task.type === 'section',
'text-hint': task.completed}"

View File

@@ -10,20 +10,96 @@ export class ChangelogComponent
{
changelog: any[] = [
// v12.3.0
{
version : 'v12.3.0',
releaseDate: 'May 07, 2021',
changes : [
{
type: 'Added',
list: [
'(apps/notes) New Notes app',
'(fuse/masonry) Added a component for creating fast Masonry-like layouts'
]
},
{
type: 'Changed',
list: [
'(apps/tasks) Tweaked the hover color on tasks list for better consistency',
'(apps/mailbox) Adjusted the app title font size for better consistency',
'(apps/mailbox) Used shadow on threads for better consistency'
]
}
]
},
// v12.2.0
{
version : 'v12.2.0',
releaseDate: 'May 01, 2021',
changes : [
{
type: 'Added',
list: [
'(apps/chat) New and improvement version of Chat app',
'(fuse/fullscreen) Added fullscreen toggle component'
]
},
{
type: 'Changed',
list: [
'(dependencies) Updated Angular, Angular Material and various other packages',
'(apps/academy) Better error handling on courses that are not exist',
'(apps/academy) Added missing trackBy functions to ngFor loops',
'(apps/mailbox) Removed unused methods',
'(pages/pricing) Improved the spacing of the CTA section on all pricing pages'
]
}
]
},
// v12.1.0
{
version : 'v12.1.0',
releaseDate: 'April 26, 2021',
changes : [
{
type: 'Added',
list: [
'(apps/academy) New and improvement version of Academy app',
'(icons) Material Solid icons'
]
},
{
type: 'Changed',
list: [
'(dependencies) Updated Angular, Angular Material and various other packages',
'(icons) Updated Heroicons',
'(icons) Updated Material Icons',
'(apps/file-manager) Better grid for File Manager app',
'(layouts/classy) Removed footer for better \'classy\' look'
]
},
{
type: 'Fixed',
list: [
'(apps/contacts) Clicking on the checkbox on Tag select panel acts weird'
]
}
]
},
// v12.0.0
{
version : 'v12.0.0',
releaseDate: 'April 16, 2021',
changes : [
{
type: 'Breaking Changes',
type: 'Added',
list: [
'This is the new major version of the Fuse and it\'s completely different from previous versions with no upgrade path',
'This version requires a clean installation'
]
},
{
type: 'Features',
type: 'Changed',
list: [
'Improved the look and feel',
'Re-wrote the entire template from scratch using Tailwind',

View File

@@ -0,0 +1,334 @@
<div class="flex flex-col flex-auto min-w-0">
<!-- Header -->
<div class="flex flex-col sm:flex-row flex-0 sm:items-center sm:justify-between p-6 sm:py-8 sm:px-10 border-b bg-card dark:bg-transparent">
<div class="flex-1 min-w-0">
<!-- Breadcrumbs -->
<div class="flex flex-wrap items-center font-medium">
<div>
<a class="whitespace-nowrap text-primary-500">Documentation</a>
</div>
<div class="flex items-center ml-1 whitespace-nowrap">
<mat-icon
class="icon-size-5 text-secondary"
[svgIcon]="'heroicons_solid:chevron-right'"></mat-icon>
<a class="ml-1 text-primary-500">Core Features</a>
</div>
<div class="flex items-center ml-1 whitespace-nowrap">
<mat-icon
class="icon-size-5 text-secondary"
[svgIcon]="'heroicons_solid:chevron-right'"></mat-icon>
<span class="ml-1 text-secondary">Components</span>
</div>
</div>
<!-- Title -->
<div class="mt-2">
<h2 class="text-3xl md:text-4xl font-extrabold tracking-tight leading-7 sm:leading-10 truncate">
Masonry
</h2>
</div>
</div>
<button
class="-ml-3 sm:ml-0 mb-2 sm:mb-0 order-first sm:order-last"
mat-icon-button
(click)="toggleDrawer()">
<mat-icon [svgIcon]="'heroicons_outline:menu'"></mat-icon>
</button>
</div>
<div class="flex-auto max-w-3xl p-6 sm:p-10 prose prose-sm">
<p>
<strong>fuse-masonry</strong> is a basic Masonry layout component to show items in Masonry grid layout. Since it doesn't use any other third party library or complex calculations to
keep everything smooth and fast;
</p>
<ul>
<li>It does NOT work with elements with different widths
<li>It does NOT sort items based on their heights (This here is the real performance killer)</li>
</ul>
<p>
If you don't need to mix and match different width items and don't need to sort items based on their heights, <strong>fuse-masonry</strong> will do the job for you and it will do it very smoothly.
</p>
<p>
<strong>Exported as: </strong><code>fuseMasonry</code>
</p>
<h2>Module</h2>
<textarea
fuse-highlight
lang="typescript">
import { FuseMasonry } from '@fuse/components/masonry';
</textarea>
<h2>Properties</h2>
<div class="bg-card py-3 px-6 rounded shadow">
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-mono text-md text-secondary">
<div>@Input()</div>
<div>columnsTemplate: TemplateRef</div>
</td>
<td>
Column template for masonry component to lay out.
</td>
<td>
<code class="whitespace-nowrap">undefined</code>
</td>
</tr>
<tr>
<td class="font-mono text-md text-secondary">
<div>@Input()</div>
<div>columns: number</div>
</td>
<td>
Number of columns to create
</td>
<td>
<code class="whitespace-nowrap">undefined</code>
</td>
</tr>
<tr>
<td class="font-mono text-md text-secondary">
<div>@Input()</div>
<div>items: any[]</div>
</td>
<td>
Items to distribute into columns
</td>
<td>
<code class="whitespace-nowrap">[]</code>
</td>
</tr>
</tbody>
</table>
</div>
<h2>Methods</h2>
<p>This component doesn't have any public methods.</p>
<h2>Usage</h2>
<p>
<strong>fuse-masonry</strong> doesn't provide a default template or styling to the items. You can think of it as a helper function but in a component form:
</p>
<div class="example-viewer">
<div class="title">
<h6>Example</h6>
</div>
<mat-tab-group [animationDuration]="'0ms'">
<mat-tab label="Preview">
<ng-template matTabContent>
<div class="bg-gray-100 p-4">
<div class="mx-auto max-w-80">
<fuse-masonry
class="-mx-2"
[items]="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
[columns]="4"
[columnsTemplate]="columnsTemplate">
</fuse-masonry>
<ng-template
#columnsTemplate
let-columns>
<ng-container *ngFor="let column of columns">
<!-- Column -->
<div class="flex-1 mx-2 p-2 border rounded space-y-2">
<ng-container *ngFor="let item of column.items">
<!-- Item -->
<div class="p-2 text-center rounded bg-primary text-on-primary">
{{item}}
</div>
</ng-container>
</div>
</ng-container>
</ng-template>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab label="HTML">
<!-- @formatter:off -->
<textarea
fuse-highlight
[lang]="'html'">
<fuse-masonry
class="-mx-2"
[items]="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
[columns]="4"
[columnsTemplate]="columnsTemplate">
</fuse-masonry>
<ng-template
#columnsTemplate
let-columns>
<ng-container *ngFor="let column of columns">
<!-- Column -->
<div class="flex-1 mx-2 p-2 border rounded space-y-2">
<ng-container *ngFor="let item of column.items">
<!-- Item -->
<div class="p-2 text-center rounded bg-primary text-on-primary">
ITEM CONTENT
</div>
</ng-container>
</div>
</ng-container>
</ng-template>
</textarea>
<!-- @formatter:on -->
</mat-tab>
</mat-tab-group>
</div>
<h3>Responsive</h3>
<p>
<strong>fuse-masonry</strong> doesn't provide a way of changing the column count depending on breakpoints but this can easily be handled by
combining <code>fuse-masonry</code> with <code>FuseMediaWatcherService</code>:
</p>
<div class="example-viewer">
<div class="title">
<h6>Example</h6>
</div>
<mat-tab-group [animationDuration]="'0ms'">
<mat-tab label="Preview">
<ng-template matTabContent>
<div class="bg-gray-100 p-4">
<div class="mx-auto max-w-80">
<fuse-masonry
class="-mx-2"
[items]="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]"
[columns]="columns"
[columnsTemplate]="columnsTemplate">
</fuse-masonry>
<ng-template
#columnsTemplate
let-columns>
<ng-container *ngFor="let column of columns">
<!-- Column -->
<div class="flex-1 mx-2 p-2 border rounded space-y-2">
<ng-container *ngFor="let item of column.items">
<!-- Item -->
<div class="p-2 text-center rounded bg-primary text-on-primary">
{{item}}
</div>
</ng-container>
</div>
</ng-container>
</ng-template>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab label="HTML">
<!-- @formatter:off -->
<textarea
fuse-highlight
[lang]="'html'">
<fuse-masonry
class="-mx-2"
[items]="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]"
[columns]="columns"
[columnsTemplate]="columnsTemplate">
</fuse-masonry>
<ng-template
#columnsTemplate
let-columns>
<ng-container *ngFor="let column of columns">
<!-- Column -->
<div class="flex-1 mx-2 p-2 border rounded space-y-2">
<ng-container *ngFor="let item of column.items">
<!-- Item -->
<div class="p-2 text-center rounded bg-primary text-on-primary">
ITEM CONTENT
</div>
</ng-container>
</div>
</ng-container>
</ng-template>
</textarea>
<!-- @formatter:on -->
</mat-tab>
<mat-tab label="TypeScript">
<!-- @formatter:off -->
<textarea
fuse-highlight
[lang]="'ts'">
columns: number = 4;
// Subscribe to media changes
this._fuseMediaWatcherService.onMediaChange$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe(({matchingAliases}) => {
// Set the masonry columns
//
// This if block structured in a way so that only the
// biggest matching alias will be used to set the column
// count.
if ( matchingAliases.includes('xl') )
{
this.columns = 5;
}
else if ( matchingAliases.includes('lg') )
{
this.columns = 4;
}
else if ( matchingAliases.includes('md') )
{
this.columns = 3;
}
else if ( matchingAliases.includes('sm') )
{
this.columns = 2;
}
else
{
this.columns = 1;
}
});
</textarea>
<!-- @formatter:on -->
</mat-tab>
</mat-tab-group>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More